[
  {
    "path": ".buildkite/hooks/pre-command",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\ncheckout_path=\"${BUILDKITE_BUILD_CHECKOUT_PATH:-$(pwd)}\"\ncache_root=\"${checkout_path}/.buildkite/cache-volume\"\n\nmkdir -p \\\n  \"${cache_root}/go/build\" \\\n  \"${cache_root}/go/pkg/mod\" \\\n  \"${cache_root}/golangci-lint\"\n\nexport GOCACHE=\"${cache_root}/go/build\"\nexport GOMODCACHE=\"${cache_root}/go/pkg/mod\"\nexport GOLANGCI_LINT_CACHE=\"${cache_root}/golangci-lint\"\n"
  },
  {
    "path": ".buildkite/pipeline.release.yml",
    "content": "agents:\n  queue: hosted\n\ncache: \".buildkite/cache-volume\"\n\nsteps:\n  - label: \":terminal: build ({{matrix}})\"\n    key: build\n    matrix:\n          - \"darwin\"\n          - \"linux\"\n          - \"windows\"\n    artifact_paths:\n      - dist/**/*\n    secrets:\n      - MISE_GITHUB_TOKEN\n      - POSTHOG_API_KEY\n      - OAUTH_CLIENT_ID\n    command: 'GOOS=\"{{matrix}}\" .buildkite/release.sh release --clean --split'\n    plugins:\n      - mise#v1.1.2: ~\n\n  - label: \":rocket: :package: upload packages ({{matrix}})\"\n    key: upload_packages\n    depends_on: build\n    matrix:\n      - \"deb\"\n      - \"rpm\"\n    command: '.buildkite/upload-packages.sh \"{{matrix}}\"'\n    plugins:\n      - artifacts#v1.9.4:\n          download:\n            - dist/linux/*\n\n  - label: \":rocket: :github: release\"\n    key: release\n    depends_on: [\"build\", \"upload_packages\"]\n    artifact_paths:\n      - dist/**/*\n    env:\n      AWS_REGION: us-east-1\n    secrets:\n      - MISE_GITHUB_TOKEN\n      - POSTHOG_API_KEY\n      - OAUTH_CLIENT_ID\n    plugins:\n      - aws-assume-role-with-web-identity#v1.6.0:\n          role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-buildkite-cli-release\n          session-tags:\n            - organization_slug\n            - organization_id\n            - pipeline_slug\n      - aws-ssm#v1.1.0:\n          parameters:\n            GITHUB_TOKEN: /pipelines/buildkite/buildkite-cli-release/github-token\n            DOCKERHUB_USER: /pipelines/buildkite/buildkite-cli-release/dockerhub-user\n            DOCKERHUB_PASSWORD: /pipelines/buildkite/buildkite-cli-release/dockerhub-password\n      - artifacts#v1.9.4:\n          download:\n            - dist/**/*\n      - mise#v1.1.2: ~\n    command: '.buildkite/release.sh continue --merge'\n"
  },
  {
    "path": ".buildkite/pipeline.yml",
    "content": "agents:\n  queue: hosted\n\ncache: \".buildkite/cache-volume\"\n\nsteps:\n  - name: \":golangci-lint: lint\"\n    key: lint\n    command: 'golangci-lint run --verbose --timeout 3m'\n    secrets:\n      - MISE_GITHUB_TOKEN\n    plugins:\n      - mise#v1.1.2: ~\n\n  - name: \":go: test\"\n    key: test\n    artifact_paths:\n      - cover-tree.svg\n    secrets:\n      - MISE_GITHUB_TOKEN\n    commands:\n      # Hosted agents inject org/token env that breaks config-precedence tests,\n      # so clear those variables in the command shell right before go test.\n      - unset BUILDKITE_ORGANIZATION_SLUG BUILDKITE_API_TOKEN\n      - go test -coverprofile cover.out ./...\n      - go-cover-treemap -coverprofile cover.out > cover-tree.svg\n      - echo '<details><summary>Coverage tree map</summary><img src=\"artifact://cover-tree.svg\" alt=\"Test coverage tree map\" width=\"70%\"></details>' | buildkite-agent annotate --style \"info\"\n    plugins:\n      - mise#v1.1.2: ~\n\n  - label: \":terminal: build ({{matrix}})\"\n    key: build\n    depends_on: [\"lint\", \"test\"]\n    branches:\n      - main\n    matrix:\n          - \"darwin\"\n          - \"linux\"\n          - \"windows\"\n    artifact_paths:\n      - dist/**/*\n    secrets:\n      - MISE_GITHUB_TOKEN\n      - POSTHOG_API_KEY\n      - OAUTH_CLIENT_ID\n    command: 'GOOS=\"{{matrix}}\" .buildkite/release.sh release --clean --snapshot --split'\n    plugins:\n      - mise#v1.1.2: ~\n\n  - input: \":package: Create a release?\"\n    key: release_unblock\n    depends_on: [\"lint\", \"test\", \"build\"]\n    prompt: \"Select the release type\"\n    branches:\n      - main\n    allowed_teams:\n      - \"support\"\n      - \"deploy\"\n    blocked_state: \"passed\"\n    fields:\n      - key: release-type\n        select: \"Release Type\"\n        required: true\n        options:\n          - label: \"Patch (v3.x.X)\"\n            value: \"patch\"\n          - label: \"Minor (v3.X.0)\"\n            value: \"minor\"\n          - label: \"Major (vX.0.0) - Manual only\"\n            value: \"major\"\n\n  # this tags the commit with the input from the previous block step and pushes it to github\n  # that will trigger the buildkite-cli-release pipeline off the tag which will create a release in github\n  - label: \":rocket: Pushing a tag to release\"\n    command: \".buildkite/tag.sh\"\n    depends_on: release_unblock\n    branches:\n      - main\n    env:\n      AWS_REGION: us-east-1\n    plugins:\n      - aws-assume-role-with-web-identity#v1.6.0:\n          role-arn: arn:aws:iam::445615400570:role/pipeline-buildkite-buildkite-cli\n          session-tags:\n            - organization_slug\n            - organization_id\n            - pipeline_slug\n      - aws-ssm#v1.1.0:\n          parameters:\n            GITHUB_TOKEN: /pipelines/buildkite/buildkite-cli/github-token\n            GITHUB_USER: /pipelines/buildkite/buildkite-cli/github-user\n"
  },
  {
    "path": ".buildkite/release.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# This script is used to build a release of the CLI and publish it to multiple registries on Buildkite\n#\n\n# NOTE: do not exit on non-zero returns codes\nset -uo pipefail\n\nexport GORELEASER_KEY=$(buildkite-agent secret get goreleaser_key)\n\nif [[ $? -ne 0 ]]; then\n    echo \"Failed to retrieve GoReleaser Pro key\"\n    exit 1\nfi\n\n# check if DOCKERHUB_USER and DOCKERHUB_PASSWORD are set if not skip docker login\nif [[ -z \"${DOCKERHUB_USER:-}\" || -z \"${DOCKERHUB_PASSWORD:-}\" ]]; then\n    echo \"Skipping Docker login as DOCKERHUB_USER or DOCKERHUB_PASSWORD is not set\"\nelse\n    echo \"--- :key: :docker: Login to Docker Hub using ko\"\n    echo \"${DOCKERHUB_PASSWORD}\" | ko login index.docker.io --username \"${DOCKERHUB_USER}\" --password-stdin\n    if [[ $? -ne 0 ]]; then\n        echo \"Docker login failed\"\n        exit 1\n    fi\nfi\n\nif ! goreleaser \"$@\"; then\n    echo \"Failed to build a release\"\n    exit 1\nfi\n"
  },
  {
    "path": ".buildkite/tag.sh",
    "content": "#!/usr/bin/env bash\n\n#\n# This script calculates the next semantic version and pushes a tag\n#\n\nset -euo pipefail\n\nRELEASE_TYPE=\"$(buildkite-agent meta-data get \"release-type\")\"\n\nif [[ \"${RELEASE_TYPE}\" == \"major\" ]]; then\n  echo \"🚨 Major releases require manual tagging to prevent accidents.\"\n  echo \"Please run: git tag vX.0.0 && git push origin vX.0.0\"\n  exit 1\nfi\n\n# Get latest tag matching v*.*.* pattern\nLATEST_TAG=$(git describe --tags --match \"v[0-9]*\" --abbrev=0 2>/dev/null) || {\n  echo \"Error: No existing version tags found. Cannot calculate next version.\"\n  exit 1\n}\necho \"Latest tag: ${LATEST_TAG}\"\n\n# Parse version (strip 'v' prefix and any pre-release suffix)\nVERSION=\"${LATEST_TAG#v}\"\nIFS='.' read -r MAJOR MINOR PATCH <<< \"${VERSION%%-*}\"\n\n# Calculate new version\ncase \"${RELEASE_TYPE}\" in\n  minor)\n    TAG=\"v${MAJOR}.$((MINOR + 1)).0\"\n    ;;\n  patch)\n    TAG=\"v${MAJOR}.${MINOR}.$((PATCH + 1))\"\n    ;;\n  *)\n    echo \"Error: Unknown release type: ${RELEASE_TYPE}\"\n    exit 1\n    ;;\nesac\n\necho \"New tag: ${TAG}\"\n\nif git ls-remote --exit-code --tags origin \"refs/tags/${TAG}\" >/dev/null 2>&1; then\n  echo \"Error: Tag ${TAG} already exists at origin\"\n  exit 1\nfi\n\necho \"${TAG} does not exist at origin. Proceeding... 🚀\"\n\necho \"--- Downloading gh\"\ncurl -sL https://github.com/cli/cli/releases/download/v2.57.0/gh_2.57.0_linux_amd64.tar.gz | tar xz\necho \"--- Logging in to gh\"\ngh_2.57.0_linux_amd64/bin/gh auth setup-git\n\necho \"+++ Tagging ${BUILDKITE_COMMIT} with ${TAG}\"\ngit tag \"${TAG}\"\ngit push origin \"${TAG}\"\n"
  },
  {
    "path": ".buildkite/upload-packages.sh",
    "content": "#!/bin/env bash\n\n#\n# This script is used to upload packages to Buildkite registries\n#\n\nset -uo pipefail\n\nif [[ -z \"${1}\" ]]; then\n    echo \"Must pass in the package type: deb, rpm\"\n    exit 1\nfi\n\nPACKAGE=${1}\nORGANIZATION=${2:-buildkite}\nREGISTRY=${3:-cli-$PACKAGE}\n\naudience() {\n    ORG=$1\n    REGISTRY=$2\n    echo \"https://packages.buildkite.com/${ORG}/${REGISTRY}\"\n}\nupload_url() {\n    ORG=$1\n    REGISTRY=$2\n    echo \"https://api.buildkite.com/v2/packages/organizations/${ORG}/registries/${REGISTRY}/packages\"\n}\n\nAUDIENCE=$(audience $ORGANIZATION $REGISTRY)\n\n# grab a token for pushing packages to buildkite with an expiry of 3 mins\necho \"--- Fetching OIDC token for $AUDIENCE\"\nTOKEN=$(buildkite-agent oidc request-token --audience \"$AUDIENCE\" --lifetime 180)\n\nif [[ $? -ne 0 ]]; then\n    echo \"Failed to retrieve OIDC token\"\n    exit 1\nfi\n\nfor FILE in dist/linux/*.${PACKAGE}; do\n    echo \"--- Pushing $FILE\"\n    if [[ $PACKAGE = \"apk\" ]]; then\n        curl -s -X POST $(upload_url $ORGANIZATION $REGISTRY) \\\n             -H \"Authorization: Bearer ${TOKEN}\" \\\n             -F \"package[distro_version_id]=alpine/v3\" \\\n             -F \"package[package_file]=@${FILE}\" \\\n            --fail-with-body\n    else\n        curl -s -X POST $(upload_url $ORGANIZATION $REGISTRY) \\\n             -H \"Authorization: Bearer ${TOKEN}\" \\\n             -F \"file=@${FILE}\" \\\n            --fail-with-body\n    fi\n\n    if [[ $? -ne 0 ]]; then\n        echo \"Failed to push package $FILE\"\n        exit 1\n    fi\ndone\n"
  },
  {
    "path": ".dockerignore",
    "content": "# Git files\n.git\n.github\n.gitignore\n\n# Documentation\n*.md\ndocs/\nimages/\nLICENSE.md\nCHANGELOG.md\nCONTRIBUTING.md\nAGENT.md\nREADME.md\n\n# Build artifacts\ndist/\nbuild-logs-*\n\n# Test files\n*_test.go\nfixtures/\ninternal/*/resolver/*_test.go\n\n# IDE and editor files\n.vscode/\n.idea/\n*.swp\n*.swo\n*~\n.DS_Store\n\n# CI/CD files\n.buildkite/\nbuildkite.yaml\n.bk.yaml\n\n# Config files not needed for build\n.golangci.yaml\n.graphqlrc.yml\ngenqlient.yaml\n\n# Dependencies (these will be downloaded during build)\nvendor/\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @buildkite/support @buildkite/engineering\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 🐛 Bug Report\ndescription: File a bug report for the Buildkite CLI\ntitle: \"[Bug]: \"\nlabels: [\"bug\", \"triage\"]\nprojects: [\"buildkite/cli\"]\nassignees:\n  - buildkite/technical-services\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n  - type: input\n    id: contact\n    attributes:\n      label: Contact Details\n      description: How can we get in touch with you if we need more info?\n      placeholder: ex. email@example.com\n    validations:\n      required: false\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: Also tell us, what did you expect to happen?\n      placeholder: Tell us what you see!\n      value: \"A bug happened!\"\n    validations:\n      required: true\n  - type: input\n    id: version\n    attributes:\n      label: Version\n      description: What version of our software are you running?\n    validations:\n      required: true\n  - type: dropdown\n    id: browsers\n    attributes:\n      label: What environment are you seeing the problem on?\n      multiple: true\n      options:\n        - CI\n        - Local Development\n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant log output\n      description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.\n      render: shell\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Buildkite Community Forum\n    url: https://forum.buildkite.community/\n    about: Discuss issues and requests in the forum.\n  - name: Contact Buildkite Support\n    url: https://buildkite.com/about/contact/\n    about: Get in contact with Buildkite's support team.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 💡 Feature Request\ndescription: Suggest an idea for this project.\ntitle: \"💡 feat: <title>\"\nlabels: [Enhancement]\nbody:\n  - type: textarea\n    attributes:\n      label: Is your feature request related to a problem?\n      description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...].\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Describe the solution you'd like.\n      description: A clear and concise description of what you want to happen.\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Describe alternatives you've considered.\n      description: A clear and concise description of any alternative solutions or features you've considered.\n    validations:\n      required: false\n\n  - type: textarea\n    attributes:\n      label: Additional context\n      description: Add any other context or screenshots about the feature request here.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "### Description\n\n<!--\n- What problem are you trying to solve, and how are you solving it?\n- What alternatives did you consider?\n-->\n\n### Changes\n\n<!--\nList of what the PR changes. If the PR changes the CLI arguments, consider adding the output of the various levels of `bk <subcomand> --help`.\n\nCan skip if changes are simple or clear from the commit messages.\n-->\n\n### Testing\n- [ ] Tests have run locally (with `go test ./...`)\n- [ ] Code is formatted (with `go fmt ./...`)\n\n\n### Disclosures / Credits\n\n<!--\nIf you used AI in any way to produce this PR (beyond typo fixes or small amounts of tab-autocompletion), please describe the extent of the contribution here, and the tools used.\nFeel free to claim credit for work _not_ done by an AI here too, or to give credit to others who helped in any meaningful way.\n\nExamples:\n - \"Claude Code wrote the unit tests, then I implemented the rest of the change\"\n - \"I consulted ChatGPT on potential approaches, then wrote the implementation myself\"\n - \"I used Gemini to write the code and Midjourney to produce the diagrams\"\n - \"Special thanks to the Wikipedia page on ANSI escape codes\"\n - \"I did not use AI tools at all\"\n-->\n"
  },
  {
    "path": ".gitignore",
    "content": "*.DS_STORE\ndist/\nbuildkite.yaml\n.bk.yaml\nbuild-logs-*\nmise.local.toml\n.mise.local.toml\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "version: \"2\"\nlinters:\n  enable:\n    - nolintlint\n    - tparallel\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    rules:\n      - linters:\n          - errcheck\n        path: _test.go\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\nformatters:\n  enable:\n    - gofmt\n    - goimports\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "project_name: bk\nversion: 2\n\nrelease:\n  name_template: Buildkite CLI {{.Version}}\n  draft: true\n  replace_existing_draft: true\n  prerelease: auto\n  make_latest: false\n  mode: replace\n\nchangelog:\n  use: github\n\nbrews:\n  - name: bk@3\n    ids:\n      - macos-archive\n      - linux-archive\n    directory: Formula\n    homepage: https://github.com/buildkite/cli\n    description: Work with Buildkite from the command-line\n    license: MIT\n    skip_upload: false\n    test: system \"#{bin}/bk version\"\n    repository:\n      owner: buildkite\n      name: homebrew-buildkite\n      branch: master\n\nbuilds:\n  - id: macos\n    goos: [darwin]\n    goarch: [amd64, arm64]\n    binary: bk\n    main: .\n    ldflags:\n      - -s -w -X github.com/buildkite/cli/v3/cmd/version.Version={{.Version}} -X github.com/buildkite/cli/v3/pkg/cmd/pipeline.MigrationEndpoint=https://m4vrh5pvtd.execute-api.us-east-1.amazonaws.com/production/migrate -X github.com/buildkite/cli/v3/pkg/analytics.apiKey={{.Env.POSTHOG_API_KEY}} -X github.com/buildkite/cli/v3/pkg/oauth.DefaultClientID={{.Env.OAUTH_CLIENT_ID}}\n\n  - id: linux\n    goos: [linux]\n    goarch: [amd64, arm64]\n    env:\n      - CGO_ENABLED=0\n    binary: bk\n    main: .\n    ldflags:\n      - -s -w -X github.com/buildkite/cli/v3/cmd/version.Version={{.Version}} -X github.com/buildkite/cli/v3/pkg/cmd/pipeline.MigrationEndpoint=https://m4vrh5pvtd.execute-api.us-east-1.amazonaws.com/production/migrate -X github.com/buildkite/cli/v3/pkg/analytics.apiKey={{.Env.POSTHOG_API_KEY}} -X github.com/buildkite/cli/v3/pkg/oauth.DefaultClientID={{.Env.OAUTH_CLIENT_ID}}\n\n  - id: windows\n    goos: [windows]\n    goarch: [amd64, arm64]\n    binary: bk\n    main: .\n    ldflags:\n      - -s -w -X github.com/buildkite/cli/v3/cmd/version.Version={{.Version}} -X github.com/buildkite/cli/v3/pkg/cmd/pipeline.MigrationEndpoint=https://m4vrh5pvtd.execute-api.us-east-1.amazonaws.com/production/migrate -X github.com/buildkite/cli/v3/pkg/analytics.apiKey={{.Env.POSTHOG_API_KEY}} -X github.com/buildkite/cli/v3/pkg/oauth.DefaultClientID={{.Env.OAUTH_CLIENT_ID}}\n\nkos:\n  - repositories:\n      - docker.io/buildkite/cli\n    build: linux\n    main: .\n    creation_time: \"{{.CommitTimestamp}}\"\n    base_image: 'cgr.dev/chainguard/static:latest'\n    tags:\n      - '{{.Version}}'\n      - latest\n    labels:\n      org.opencontainers.image.authors: Buildkite Inc. https://buildkite.com\n      org.opencontainers.image.source: https://github.com/buildkite/cli\n      org.opencontainers.image.created: \"{{.Date}}\"\n      org.opencontainers.image.title: \"{{.ProjectName}}\"\n      org.opencontainers.image.revision: \"{{.FullCommit}}\"\n      org.opencontainers.image.version: \"{{.Version}}\"\n    bare: true\n    preserve_import_paths: false\n    disable: '{{ and (isEnvSet \"GOOS\") (ne .Env.GOOS \"linux\") }}'\n    platforms:\n      - linux/amd64\n      - linux/arm64\n\narchives:\n  - id: macos-archive\n    builds: [macos]\n    name_template: \"bk_{{ .Version }}_macOS_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}\"\n    wrap_in_directory: true\n    format: zip\n    files:\n      - LICENSE.md\n      - README.md\n\n  - id: linux-archive\n    builds: [linux]\n    name_template: \"bk_{{ .Version }}_linux_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}\"\n    wrap_in_directory: true\n    format: tar.gz\n    files:\n      - LICENSE.md\n      - README.md\n\n  - id: windows-archive\n    builds: [windows]\n    name_template: \"bk_{{ .Version }}_windows_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}\"\n    wrap_in_directory: false\n    format: zip\n    files:\n      - LICENSE.md\n      - README.md\n\nnfpms:\n  - builds:\n      - linux\n    vendor: Buildkite\n    homepage: https://buildkite.com\n    maintainer: Buildkite <support@buildkite.com>\n    description: A command line interface for Buildkite.\n    license: MIT\n    formats:\n      - apk\n      - deb\n      - rpm\n    provides:\n      - bk\n# vim: set ts=2 sw=2 tw=0 fo=cnqoj\n"
  },
  {
    "path": ".graphqlrc.yml",
    "content": "schema: schema.graphql\n"
  },
  {
    "path": "AGENT.md",
    "content": "This project is the Buildkite CLI (`bk`)\n\n## Commands\n- Bootstrap: `mise install`\n- Hooks: `mise run hooks`\n- Format: `mise run format`\n- Test: `mise run test`\n- Lint: `mise run lint`\n- Generate: `mise run generate` (required after GraphQL changes)\n- Run: `go run main.go`\n\n## Environment\n- `BUILDKITE_GRAPHQL_TOKEN` required for development\n\n## Project Structure\n- Main binary: `main.go`\n- GraphQL schema: `schema.graphql`\n- CLI commands: `pkg/cmd/`\n\n## Notes\n- `mise.toml` pins the local Go/tool versions\n- CI: https://buildkite.com/buildkite/buildkite-cli\n- Always format after changing code\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nWe welcome contributions from the community to make Buildkite CLI, `bk`, project even better.\n\n## Getting Started\n\nTo get started with contributing, please follow these steps:\n\n1. Fork the repository.\n2. Create a feature branch with a nice name (`git checkout -b cli-new-feature`) for your changes.\n3. Install [mise](https://mise.jdx.dev/) and run `mise install`.\n4. Install the local git hooks with `mise run hooks`.\n5. Write your code.\n6. Run the local checks before opening a pull request.\n   * Format the code with `mise run format`.\n   * Lint with `mise run lint`.\n   * Make sure the tests pass with `mise run test`.\n   * Run `mise run generate` after GraphQL changes. If you need to refresh `schema.graphql`, set `BUILDKITE_GRAPHQL_TOKEN` first.\n7. Commit your changes and push them to your forked repository.\n8. Submit a pull request with a detailed description of your changes and links to any relevant issues.\n\nThe team maintaining this codebase will review your PR and start a CI build for it. For security reasons, we don't automatically run CI against forked repos, and a human will review your PR prior to its CI running.\n\n## Testing\n\nThere is a continuous integration pipeline on Buildkite:\n\nhttps://buildkite.com/buildkite/buildkite-cli\n\n## Releasing\n\nBuilds on `main` include a block step to \"Create a release\". The step takes a tag name, then takes care of tagging the built commit.\n\nNew tags trigger the release pipeline:\n\nhttps://buildkite.com/buildkite/buildkite-cli-release\n\nThis will prepare a new draft release on GitHub:\n\nhttps://github.com/buildkite/cli/releases\n\nTo release, edit the draft and _Publish release_.\n\n## Reporting Issues\n\nIf you encounter any issues or have suggestions for improvements, please open an issue on the GitHub repository. Provide as much detail as possible, including steps to reproduce the issue.\n\n## Contact\n\nIf we're really dragging our feet on reviewing a PR, please feel free to ping us through GitHub or Slack, or get in touch with [support@buildkite.com](mailto:support@buildkite.com), and they can bug us to get things done :) \n\nHappy contributing!\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.26-alpine AS base\n\nRUN apk add --no-cache git ca-certificates\n\nWORKDIR /base\n\nCOPY go.mod go.sum ./\n\nRUN go mod download\n\nCOPY . .\n\nRUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=\"-w -s\" -o bk .\n\nFROM alpine:latest\n\nRUN apk --no-cache add ca-certificates\n\nWORKDIR /cli\n\nCOPY --from=base /base/bk .\n\nENV PATH=\"/cli:${PATH}\"\n\nENTRYPOINT [\"bk\"]\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2014-2023 Buildkite Pty Ltd\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# bk - The Buildkite CLI\n\n[![Latest Release](https://img.shields.io/github/v/release/buildkite/cli?include_prereleases&sort=semver&display_name=release&logo=buildkite)](https://github.com/buildkite/cli/releases)\n\nA command line interface for [Buildkite](https://buildkite.com/).\n\nFull documentation is available at [buildkite.com/docs/platform/cli](https://buildkite.com/docs/platform/cli).\n\n## Quick Start\n\n### Install\n\n```sh\nbrew tap buildkite/buildkite && brew install buildkite/buildkite/bk\n```\n\nOr download a binary from the [releases page](https://github.com/buildkite/cli/releases).\n\n### Authenticate\n\n```sh\nbk auth login\n```\n\n## Feedback\n\nWe'd love to hear any feedback and questions you might have. Please [file an issue on GitHub](https://github.com/buildkite/cli/issues) and let us know!\n\n## Development\n\nThis repository uses [mise](https://mise.jdx.dev/) to pin Go and the main\nlocal development tools.\n\n```bash\ngit clone git@github.com:buildkite/cli.git\ncd cli/\nmise install\nmise run build\nmise run install\nmise run install:global\nmise run hooks\nmise run format\nmise run lint\nmise run test\nmise run generate\ngo run main.go --help\n```\n\n`mise.toml` pins the shared toolchain, including the release helpers used in\nCI. The module itself remains compatible with Go `1.25.0` as declared in\n`go.mod`.\n"
  },
  {
    "path": "cmd/agent/agent_test.go",
    "content": "package agent\n\nimport (\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc TestParseAgentArg(t *testing.T) {\n\tt.Parallel()\n\n\ttestcases := map[string]struct {\n\t\turl, org, agent string\n\t}{\n\t\t\"slug\": {\n\t\t\turl:   \"buildkite/abcd\",\n\t\t\torg:   \"buildkite\",\n\t\t\tagent: \"abcd\",\n\t\t},\n\t\t\"id\": {\n\t\t\turl:   \"abcd\",\n\t\t\torg:   \"testing\",\n\t\t\tagent: \"abcd\",\n\t\t},\n\t\t\"url\": {\n\t\t\turl:   \"https://buildkite.com/organizations/buildkite/agents/018a4a65-bfdb-4841-831a-ff7c1ddbad99\",\n\t\t\torg:   \"buildkite\",\n\t\t\tagent: \"018a4a65-bfdb-4841-831a-ff7c1ddbad99\",\n\t\t},\n\t\t\"clustered url\": {\n\t\t\turl:   \"https://buildkite.com/organizations/buildkite/clusters/0b7c9944-10ba-434d-9dbb-b332431252de/queues/3d039cf8-9862-4cb0-82cd-fc5c497a265a/agents/018c3d31-1b4a-454a-87f6-190b206e3759\",\n\t\t\torg:   \"buildkite\",\n\t\t\tagent: \"018c3d31-1b4a-454a-87f6-190b206e3759\",\n\t\t},\n\t}\n\n\tfor name, testcase := range testcases {\n\t\ttestcase := testcase\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\t\tconf.SelectOrganization(\"testing\", true)\n\t\t\torg, agent := parseAgentArg(testcase.url, conf)\n\n\t\t\tif org != testcase.org {\n\t\t\t\tt.Error(\"parsed organization slug did not match expected\")\n\t\t\t}\n\t\t\tif agent != testcase.agent {\n\t\t\t\tt.Error(\"parsed agent ID did not match expected\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/agent/install.go",
    "content": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\n\tbkAgent \"github.com/buildkite/cli/v3/internal/agent\"\n)\n\nvar (\n\tuserArch = runtime.GOARCH\n\tuserOS   = runtime.GOOS\n)\n\n// InstallCmd allows users to define which agent version they want to install\n// We will take care of OS/arch in the command itself\ntype InstallCmd struct {\n\tVersion     string `help:\"Specify an agent version to install\" default:\"latest\"`\n\tDest        string `help:\"Destination directory for the binary\" type:\"path\"`\n\tClusterUUID string `help:\"Cluster UUID to create the agent token on (default: the \\\"Default\\\" cluster)\" name:\"cluster-uuid\" optional:\"\"`\n\tNoToken     bool   `help:\"Skip creating an agent token and config file\" name:\"no-token\"`\n\tConfigPath  string `help:\"Path to write the agent config file\" type:\"path\"`\n}\n\nfunc (i *InstallCmd) Help() string {\n\treturn `Install the buildkite-agent binary locally.\n\nBy default, this also creates an agent token on the Default cluster and writes\na minimal config file so the agent is ready to start.\n\nExamples:\n  # Install the latest version of the agent\n  $ bk agent install\n\n  # Install a specific version\n  $ bk agent install --version \"3.112.0\"\n\n  # Install to a custom location\n  $ bk agent install --dest ~/.local/bin\n\n  # Install without creating a token/config\n  $ bk agent install --no-token\n`\n}\n\nfunc (i *InstallCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tdest := i.Dest\n\tif dest == \"\" {\n\t\tdest = bkAgent.DefaultBinDir(userOS)\n\t}\n\n\t// Check for existing installations in PATH\n\tif existing := bkAgent.FindExisting(userOS); existing != nil {\n\t\tdestBinary := filepath.Join(dest, bkAgent.BinaryName(userOS))\n\t\tif existing.Path != destBinary {\n\t\t\tfmt.Printf(\"Warning: existing buildkite-agent found at %s\", existing.Path)\n\t\t\tif existing.Version != \"\" {\n\t\t\t\tfmt.Printf(\" (%s)\", existing.Version)\n\t\t\t}\n\t\t\tfmt.Println()\n\t\t\tfmt.Printf(\"  The new install at %s may be shadowed in your PATH.\\n\", destBinary)\n\t\t\tfmt.Println()\n\t\t}\n\t}\n\n\tif err := os.MkdirAll(dest, 0o755); err != nil {\n\t\treturn fmt.Errorf(\"creating destination directory: %w\", err)\n\t}\n\n\tversion := i.Version\n\tif version == \"latest\" {\n\t\tresolved, err := bkAgent.ResolveLatestVersion()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"resolving latest version: %w\", err)\n\t\t}\n\t\tversion = resolved\n\t}\n\n\tversion = strings.TrimPrefix(version, \"v\")\n\n\tdownloadURL := bkAgent.BuildDownloadURL(version, userOS, userArch)\n\tfmt.Printf(\"Downloading buildkite-agent v%s for %s/%s...\\n\", version, userOS, userArch)\n\n\ttmpFile, err := bkAgent.DownloadToTemp(downloadURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"downloading agent: %w\", err)\n\t}\n\tdefer os.Remove(tmpFile)\n\n\t// Verify the download checksum\n\tfmt.Println(\"Verifying checksum...\")\n\tsumsURL := bkAgent.BuildSHA256SumsURL(version)\n\tarchiveFilename := filepath.Base(downloadURL)\n\texpectedHash, err := bkAgent.FetchExpectedSHA256(sumsURL, archiveFilename)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"fetching checksum: %w\", err)\n\t}\n\tif err := bkAgent.VerifySHA256(tmpFile, expectedHash); err != nil {\n\t\treturn fmt.Errorf(\"checksum verification failed: %w\", err)\n\t}\n\n\tif err := bkAgent.ExtractBinary(tmpFile, dest, userOS); err != nil {\n\t\treturn fmt.Errorf(\"extracting agent: %w\", err)\n\t}\n\n\tbinaryName := bkAgent.BinaryName(userOS)\n\tfmt.Printf(\"Installed buildkite-agent to %s\\n\", filepath.Join(dest, binaryName))\n\n\tif !i.NoToken {\n\t\tif err := i.createTokenAndConfig(globals); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (i *InstallCmd) createTokenAndConfig(globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"initializing API client: %w\", err)\n\t}\n\n\tctx := context.Background()\n\torg := f.Config.OrganizationSlug()\n\n\tclusterID, err := bkAgent.FindCluster(ctx, f, org, i.ClusterUUID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"finding default cluster: %w\", err)\n\t}\n\n\tfmt.Println(\"Creating agent token...\")\n\ttoken, err := bkAgent.CreateAgentToken(ctx, f, org, clusterID, \"Token created by bk agent install\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating agent token: %w\", err)\n\t}\n\n\tconfigPath := i.ConfigPath\n\tif configPath == \"\" {\n\t\tconfigPath = bkAgent.DefaultConfigPath(userOS)\n\t}\n\n\tbuildPath := bkAgent.DefaultBuildPath(userOS)\n\tif err := bkAgent.WriteAgentConfig(configPath, token, buildPath, nil); err != nil {\n\t\treturn fmt.Errorf(\"writing agent config: %w\", err)\n\t}\n\n\tfmt.Printf(\"Agent config written to %s\\n\", configPath)\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/agent/list.go",
    "content": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nconst (\n\tstateRunning = \"running\"\n\tstateIdle    = \"idle\"\n\tstatePaused  = \"paused\"\n)\n\nvar validStates = []string{stateRunning, stateIdle, statePaused}\n\ntype ListCmd struct {\n\tName     string   `help:\"Filter agents by their name\"`\n\tVersion  string   `help:\"Filter agents by their version\"`\n\tHostname string   `help:\"Filter agents by their hostname\"`\n\tState    string   `help:\"Filter agents by state (running, idle, paused)\"`\n\tTags     []string `help:\"Filter agents by tags\"`\n\tPerPage  int      `help:\"Number of agents per page\" default:\"30\"`\n\tLimit    int      `help:\"Maximum number of agents to return\" default:\"100\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ListCmd) Help() string {\n\treturn `By default, shows up to 100 agents. Use filters to narrow results, or increase the number of agents displayed with --limit.\n\nExamples:\n  # List all agents\n  $ bk agent list\n\n  # List agents with JSON output\n  $ bk agent list --output json\n\n  # List only running agents (currently executing jobs)\n  $ bk agent list --state running\n\n  # List only idle agents (connected but not running jobs)\n  $ bk agent list --state idle\n\n  # List only paused agents\n  $ bk agent list --state paused\n\n  # Filter agents by hostname\n  $ bk agent list --hostname my-server-01\n\n  # Combine state and hostname filters\n  $ bk agent list --state idle --hostname my-server-01\n\n  # Filter agents by tags\n  $ bk agent list --tags queue=default\n\n  # Filter agents by multiple tags (all must match)\n  $ bk agent list --tags queue=default --tags os=linux\n\n  # Multiple filters with output format\n  $ bk agent list --state running --version 3.107.2 --output json`\n}\n\nfunc (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\tif err := validateState(c.State); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tagents := []buildkite.Agent{}\n\tpage := 1\n\thasMore := false\n\tvar previousFirstAgentID string\n\n\tfor len(agents) < c.Limit {\n\t\topts := buildkite.AgentListOptions{\n\t\t\tName:     c.Name,\n\t\t\tHostname: c.Hostname,\n\t\t\tVersion:  c.Version,\n\t\t\tListOptions: buildkite.ListOptions{\n\t\t\t\tPage:    page,\n\t\t\t\tPerPage: c.PerPage,\n\t\t\t},\n\t\t}\n\n\t\tpageAgents, _, err := f.RestAPIClient.Agents.List(ctx, f.Config.OrganizationSlug(), &opts)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif len(pageAgents) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tif page > 1 && len(pageAgents) > 0 && pageAgents[0].ID == previousFirstAgentID {\n\t\t\treturn fmt.Errorf(\"API returned duplicate page content at page %d, stopping pagination to prevent infinite loop\", page)\n\t\t}\n\t\tif len(pageAgents) > 0 {\n\t\t\tpreviousFirstAgentID = pageAgents[0].ID\n\t\t}\n\n\t\tfiltered := filterAgents(pageAgents, c.State, c.Tags)\n\t\tagents = append(agents, filtered...)\n\n\t\t// If this was a full page, there might be more results\n\t\t// We'll check after breaking from the loop if we hit the limit with a full page\n\t\tif len(pageAgents) < c.PerPage {\n\t\t\tbreak\n\t\t}\n\n\t\t// Check if we've hit the limit before fetching more\n\t\tif len(agents) >= c.Limit {\n\t\t\t// We hit the limit with a full page, so there are likely more results\n\t\t\thasMore = true\n\t\t\tbreak\n\t\t}\n\n\t\tpage++\n\t}\n\n\ttotalFetched := len(agents)\n\tif len(agents) > c.Limit {\n\t\tagents = agents[:c.Limit]\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, agents, format)\n\t}\n\n\tif len(agents) == 0 {\n\t\tfmt.Println(\"No agents found\")\n\t\treturn nil\n\t}\n\n\theaders := []string{\"State\", \"Name\", \"Version\", \"Queue\", \"Hostname\"}\n\trows := make([][]string, len(agents))\n\tfor i, agent := range agents {\n\t\tqueue := extractQueue(agent.Metadata)\n\t\tstate := displayState(agent)\n\t\trows[i] = []string{\n\t\t\tstate,\n\t\t\tagent.Name,\n\t\t\tagent.Version,\n\t\t\tqueue,\n\t\t\tagent.Hostname,\n\t\t}\n\t}\n\n\tcolumnStyles := map[string]string{\n\t\t\"state\":    \"bold\",\n\t\t\"name\":     \"bold\",\n\t\t\"hostname\": \"dim\",\n\t\t\"version\":  \"italic\",\n\t\t\"queue\":    \"italic\",\n\t}\n\ttable := output.Table(headers, rows, columnStyles)\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() {\n\t\t_ = cleanup()\n\t}()\n\n\ttotalDisplay := fmt.Sprintf(\"%d\", totalFetched)\n\tif hasMore {\n\t\ttotalDisplay = fmt.Sprintf(\"%d+\", totalFetched)\n\t}\n\tfmt.Fprintf(writer, \"Showing %d of %s agents in %s\\n\\n\", len(agents), totalDisplay, f.Config.OrganizationSlug())\n\tfmt.Fprint(writer, table)\n\n\treturn nil\n}\n\nfunc validateState(state string) error {\n\tif state == \"\" {\n\t\treturn nil\n\t}\n\n\tnormalized := strings.ToLower(state)\n\tif slices.Contains(validStates, normalized) {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"invalid state %q: must be one of %s, %s, or %s\", state, stateRunning, stateIdle, statePaused)\n}\n\nfunc filterAgents(agents []buildkite.Agent, state string, tags []string) []buildkite.Agent {\n\tfiltered := make([]buildkite.Agent, 0, len(agents))\n\tfor _, a := range agents {\n\t\tif matchesState(a, state) && matchesTags(a, tags) {\n\t\t\tfiltered = append(filtered, a)\n\t\t}\n\t}\n\treturn filtered\n}\n\nfunc matchesState(a buildkite.Agent, state string) bool {\n\tif state == \"\" {\n\t\treturn true\n\t}\n\n\tnormalized := strings.ToLower(state)\n\tswitch normalized {\n\tcase stateRunning:\n\t\treturn a.Job != nil\n\tcase stateIdle:\n\t\treturn a.Job == nil && (a.Paused == nil || !*a.Paused)\n\tcase statePaused:\n\t\treturn a.Paused != nil && *a.Paused\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc matchesTags(a buildkite.Agent, tags []string) bool {\n\tif len(tags) == 0 {\n\t\treturn true\n\t}\n\n\tfor _, tag := range tags {\n\t\tif !hasTag(a.Metadata, tag) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc hasTag(metadata []string, tag string) bool {\n\treturn slices.Contains(metadata, tag)\n}\n\nfunc extractQueue(metadata []string) string {\n\tfor _, m := range metadata {\n\t\tif after, ok := strings.CutPrefix(m, \"queue=\"); ok {\n\t\t\treturn after\n\t\t}\n\t}\n\treturn \"default\"\n}\n\nfunc displayState(a buildkite.Agent) string {\n\tif a.Job != nil {\n\t\treturn stateRunning\n\t}\n\n\tif a.Paused != nil && *a.Paused {\n\t\treturn statePaused\n\t}\n\n\treturn stateIdle\n}\n"
  },
  {
    "path": "cmd/agent/list_test.go",
    "content": "package agent\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc testFilterAgents(agents []buildkite.Agent, state string, tags []string) []buildkite.Agent {\n\treturn filterAgents(agents, state, tags)\n}\n\nfunc TestCmdAgentList(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"fetches agents through API\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tpaused := false\n\t\tagents := []buildkite.Agent{\n\t\t\t{\n\t\t\t\tID:             \"123\",\n\t\t\t\tName:           \"my-agent\",\n\t\t\t\tConnectedState: \"connected\",\n\t\t\t\tVersion:        \"3.50.0\",\n\t\t\t\tHostname:       \"host1\",\n\t\t\t\tMetadata:       []string{\"queue=default\"},\n\t\t\t\tPaused:         &paused,\n\t\t\t},\n\t\t\t{\n\t\t\t\tID:             \"456\",\n\t\t\t\tName:           \"another-agent\",\n\t\t\t\tConnectedState: \"idle\",\n\t\t\t\tVersion:        \"3.51.0\",\n\t\t\t\tHostname:       \"host2\",\n\t\t\t\tMetadata:       []string{\"queue=deploy\", \"os=linux\"},\n\t\t\t\tPaused:         &paused,\n\t\t\t},\n\t\t}\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tpage := r.URL.Query().Get(\"page\")\n\t\t\tif page == \"\" || page == \"1\" {\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\tjson.NewEncoder(w).Encode(agents)\n\t\t\t} else {\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.Agent{})\n\t\t\t}\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tctx := context.Background()\n\t\tfetchedAgents, _, err := client.Agents.List(ctx, \"test-org\", &buildkite.AgentListOptions{\n\t\t\tListOptions: buildkite.ListOptions{Page: 1, PerPage: 30},\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(fetchedAgents) != 2 {\n\t\t\tt.Fatalf(\"expected 2 agents, got %d\", len(fetchedAgents))\n\t\t}\n\n\t\tif fetchedAgents[0].Name != \"my-agent\" {\n\t\t\tt.Errorf(\"expected first agent name 'my-agent', got %q\", fetchedAgents[0].Name)\n\t\t}\n\n\t\tif fetchedAgents[1].Hostname != \"host2\" {\n\t\t\tt.Errorf(\"expected second agent hostname 'host2', got %q\", fetchedAgents[1].Hostname)\n\t\t}\n\n\t\tqueue := extractQueue(fetchedAgents[0].Metadata)\n\t\tif queue != \"default\" {\n\t\t\tt.Errorf(\"expected queue 'default', got %q\", queue)\n\t\t}\n\n\t\tdeployQueue := extractQueue(fetchedAgents[1].Metadata)\n\t\tif deployQueue != \"deploy\" {\n\t\t\tt.Errorf(\"expected queue 'deploy', got %q\", deployQueue)\n\t\t}\n\t})\n\n\tt.Run(\"renders table output\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tpaused := false\n\t\tagents := []buildkite.Agent{\n\t\t\t{\n\t\t\t\tID:             \"agent-1\",\n\t\t\t\tName:           \"test-agent\",\n\t\t\t\tConnectedState: \"connected\",\n\t\t\t\tJob:            &buildkite.Job{ID: \"job-1\"},\n\t\t\t\tVersion:        \"3.50.0\",\n\t\t\t\tHostname:       \"test-host\",\n\t\t\t\tMetadata:       []string{\"queue=default\"},\n\t\t\t\tPaused:         &paused,\n\t\t\t},\n\t\t}\n\n\t\theaders := []string{\"State\", \"Name\", \"Version\", \"Queue\", \"Hostname\"}\n\t\trows := make([][]string, len(agents))\n\t\tfor i, agent := range agents {\n\t\t\tqueue := extractQueue(agent.Metadata)\n\t\t\tstate := displayState(agent)\n\t\t\trows[i] = []string{\n\t\t\t\tstate,\n\t\t\t\tagent.Name,\n\t\t\t\tagent.Version,\n\t\t\t\tqueue,\n\t\t\t\tagent.Hostname,\n\t\t\t}\n\t\t}\n\n\t\tcolumnStyles := map[string]string{\n\t\t\t\"state\":    \"bold\",\n\t\t\t\"name\":     \"bold\",\n\t\t\t\"hostname\": \"dim\",\n\t\t\t\"version\":  \"italic\",\n\t\t\t\"queue\":    \"italic\",\n\t\t}\n\t\ttable := output.Table(headers, rows, columnStyles)\n\n\t\tif !strings.Contains(table, \"STATE\") {\n\t\t\tt.Error(\"expected table to contain header 'STATE'\")\n\t\t}\n\t\tif !strings.Contains(table, \"test-agent\") {\n\t\t\tt.Error(\"expected table to contain agent name\")\n\t\t}\n\t\tif !strings.Contains(table, \"running\") {\n\t\t\tt.Error(\"expected table to contain semantic state 'running'\")\n\t\t}\n\t\tif !strings.Contains(table, \"3.50.0\") {\n\t\t\tt.Error(\"expected table to contain version\")\n\t\t}\n\t\tif !strings.Contains(table, \"default\") {\n\t\t\tt.Error(\"expected table to contain queue\")\n\t\t}\n\t\tif !strings.Contains(table, \"test-host\") {\n\t\t\tt.Error(\"expected table to contain hostname\")\n\t\t}\n\t})\n\n\tt.Run(\"empty result returns empty array\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode([]buildkite.Agent{})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tctx := context.Background()\n\t\tagents, _, err := client.Agents.List(ctx, \"test-org\", &buildkite.AgentListOptions{\n\t\t\tListOptions: buildkite.ListOptions{Page: 1, PerPage: 30},\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(agents) != 0 {\n\t\t\tt.Errorf(\"expected 0 agents, got %d\", len(agents))\n\t\t}\n\t})\n}\n\nfunc TestAgentListStateFilter(t *testing.T) {\n\tt.Parallel()\n\n\tpaused := true\n\tnotPaused := false\n\n\tagents := []buildkite.Agent{\n\t\t{ID: \"1\", Name: \"running-agent\", Job: &buildkite.Job{ID: \"job-1\"}},\n\t\t{ID: \"2\", Name: \"idle-agent\"},\n\t\t{ID: \"3\", Name: \"paused-agent\", Paused: &paused},\n\t\t{ID: \"4\", Name: \"idle-not-paused\", Paused: &notPaused},\n\t}\n\n\ttests := []struct {\n\t\tstate string\n\t\twant  []string // agent IDs\n\t}{\n\t\t{\"running\", []string{\"1\"}},\n\t\t{\"RUNNING\", []string{\"1\"}},\n\t\t{\"idle\", []string{\"2\", \"4\"}},\n\t\t{\"paused\", []string{\"3\"}},\n\t\t{\"\", []string{\"1\", \"2\", \"3\", \"4\"}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.state, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult := testFilterAgents(agents, tt.state, nil)\n\n\t\t\tif len(result) != len(tt.want) {\n\t\t\t\tt.Errorf(\"got %d agents, want %d\", len(result), len(tt.want))\n\t\t\t}\n\n\t\t\tfor i, id := range tt.want {\n\t\t\t\tif i >= len(result) || result[i].ID != id {\n\t\t\t\t\tt.Errorf(\"agent %d: got ID %q, want %q\", i, result[i].ID, id)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAgentListInvalidState(t *testing.T) {\n\tt.Parallel()\n\n\terr := validateState(\"invalid\")\n\tif err == nil {\n\t\tt.Fatal(\"expected error for invalid state, got nil\")\n\t}\n\n\tif !strings.Contains(err.Error(), \"invalid state\") {\n\t\tt.Errorf(\"expected error to mention 'invalid state', got: %v\", err)\n\t}\n}\n\nfunc TestDisplayState(t *testing.T) {\n\tt.Parallel()\n\n\tpaused := true\n\tnotPaused := false\n\n\ttests := []struct {\n\t\tname  string\n\t\tagent buildkite.Agent\n\t\twant  string\n\t}{\n\t\t{\n\t\t\tname:  \"running when job present\",\n\t\t\tagent: buildkite.Agent{Job: &buildkite.Job{ID: \"job-1\"}},\n\t\t\twant:  stateRunning,\n\t\t},\n\t\t{\n\t\t\tname:  \"paused when paused flag\",\n\t\t\tagent: buildkite.Agent{Paused: &paused},\n\t\t\twant:  statePaused,\n\t\t},\n\t\t{\n\t\t\tname:  \"idle default\",\n\t\t\tagent: buildkite.Agent{Paused: &notPaused},\n\t\t\twant:  stateIdle,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := displayState(tt.agent)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Fatalf(\"displayState() = %q, want %q\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAgentListTagsFilter(t *testing.T) {\n\tt.Parallel()\n\n\tagents := []buildkite.Agent{\n\t\t{ID: \"1\", Name: \"default-linux\", Metadata: []string{\"queue=default\", \"os=linux\"}},\n\t\t{ID: \"2\", Name: \"deploy-macos\", Metadata: []string{\"queue=deploy\", \"os=macos\"}},\n\t\t{ID: \"3\", Name: \"default-macos\", Metadata: []string{\"queue=default\", \"os=macos\"}},\n\t\t{ID: \"4\", Name: \"no-metadata\"},\n\t}\n\n\ttests := []struct {\n\t\tname string\n\t\ttags []string\n\t\twant []string\n\t}{\n\t\t{\"single tag\", []string{\"queue=default\"}, []string{\"1\", \"3\"}},\n\t\t{\"multiple tags AND\", []string{\"queue=default\", \"os=linux\"}, []string{\"1\"}},\n\t\t{\"no match\", []string{\"queue=nonexistent\"}, []string{}},\n\t\t{\"no tags filter\", []string{}, []string{\"1\", \"2\", \"3\", \"4\"}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult := testFilterAgents(agents, \"\", tt.tags)\n\n\t\t\tif len(result) != len(tt.want) {\n\t\t\t\tt.Errorf(\"got %d agents, want %d\", len(result), len(tt.want))\n\t\t\t}\n\n\t\t\tfor i, id := range tt.want {\n\t\t\t\tif i >= len(result) || result[i].ID != id {\n\t\t\t\t\tt.Errorf(\"agent %d: got ID %q, want %q\", i, result[i].ID, id)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAgentListPagination(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"stops on partial page\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Mock server that returns 30 agents on page 1, 15 on page 2\n\t\tcallCount := 0\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tcallCount++\n\t\t\tpage := r.URL.Query().Get(\"page\")\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tswitch page {\n\t\t\tcase \"\", \"1\":\n\t\t\t\tagents := make([]buildkite.Agent, 30)\n\t\t\t\tfor i := range agents {\n\t\t\t\t\tagents[i] = buildkite.Agent{ID: fmt.Sprintf(\"page1-agent-%d\", i), Name: \"agent\"}\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(agents)\n\t\t\tcase \"2\":\n\t\t\t\tagents := make([]buildkite.Agent, 15)\n\t\t\t\tfor i := range agents {\n\t\t\t\t\tagents[i] = buildkite.Agent{ID: fmt.Sprintf(\"page2-agent-%d\", i), Name: \"agent\"}\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(agents)\n\t\t\tdefault:\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.Agent{})\n\t\t\t}\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// Simulate pagination loop\n\t\tvar agents []buildkite.Agent\n\t\tpage := 1\n\t\tlimit := 100\n\t\tperPage := 30\n\t\tvar previousFirstAgentID string\n\n\t\tfor len(agents) < limit {\n\t\t\topts := &buildkite.AgentListOptions{\n\t\t\t\tListOptions: buildkite.ListOptions{\n\t\t\t\t\tPage:    page,\n\t\t\t\t\tPerPage: perPage,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tpageAgents, _, err := client.Agents.List(context.Background(), \"test-org\", opts)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif len(pageAgents) == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif page > 1 && len(pageAgents) > 0 && pageAgents[0].ID == previousFirstAgentID {\n\t\t\t\tt.Fatal(\"detected duplicate page\")\n\t\t\t}\n\t\t\tif len(pageAgents) > 0 {\n\t\t\t\tpreviousFirstAgentID = pageAgents[0].ID\n\t\t\t}\n\n\t\t\tagents = append(agents, pageAgents...)\n\n\t\t\t// Natural pagination end\n\t\t\tif len(pageAgents) < perPage {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpage++\n\t\t}\n\n\t\t// Should have fetched 45 agents total (30 + 15)\n\t\tif len(agents) != 45 {\n\t\t\tt.Errorf(\"expected 45 agents, got %d\", len(agents))\n\t\t}\n\n\t\t// Should have made exactly 2 API calls (page 1 and page 2)\n\t\tif callCount != 2 {\n\t\t\tt.Errorf(\"expected 2 API calls, got %d\", callCount)\n\t\t}\n\t})\n\n\tt.Run(\"detects duplicate pages\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Mock server that returns same page twice\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t// Always return same agents regardless of page\n\t\t\tagents := []buildkite.Agent{\n\t\t\t\t{ID: \"agent-1\", Name: \"test\"},\n\t\t\t\t{ID: \"agent-2\", Name: \"test\"},\n\t\t\t}\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(agents)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// Simulate pagination loop\n\t\tvar agents []buildkite.Agent\n\t\tpage := 1\n\t\tlimit := 100\n\t\tperPage := 30\n\t\tvar previousFirstAgentID string\n\t\tduplicateDetected := false\n\n\t\tfor len(agents) < limit && page < 5 {\n\t\t\topts := &buildkite.AgentListOptions{\n\t\t\t\tListOptions: buildkite.ListOptions{\n\t\t\t\t\tPage:    page,\n\t\t\t\t\tPerPage: perPage,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tpageAgents, _, err := client.Agents.List(context.Background(), \"test-org\", opts)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif len(pageAgents) == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\t// Detect duplicate\n\t\t\tif page > 1 && len(pageAgents) > 0 && pageAgents[0].ID == previousFirstAgentID {\n\t\t\t\tduplicateDetected = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif len(pageAgents) > 0 {\n\t\t\t\tpreviousFirstAgentID = pageAgents[0].ID\n\t\t\t}\n\n\t\t\tagents = append(agents, pageAgents...)\n\t\t\tpage++\n\t\t}\n\n\t\tif !duplicateDetected {\n\t\t\tt.Error(\"expected duplicate page detection to trigger\")\n\t\t}\n\t})\n\n\tt.Run(\"continues on full pages with different content\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Mock server that returns different full pages\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tpage := r.URL.Query().Get(\"page\")\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tagents := make([]buildkite.Agent, 30)\n\t\t\tprefix := \"a\"\n\t\t\tswitch page {\n\t\t\tcase \"2\":\n\t\t\t\tprefix = \"b\"\n\t\t\tcase \"3\":\n\t\t\t\tprefix = \"c\"\n\t\t\t}\n\n\t\t\tfor i := range agents {\n\t\t\t\tagents[i] = buildkite.Agent{\n\t\t\t\t\tID:   fmt.Sprintf(\"%s-agent-%d\", prefix, i),\n\t\t\t\t\tName: \"agent\",\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif page == \"3\" {\n\t\t\t\t// Make page 3 partial to end pagination\n\t\t\t\tagents = agents[:10]\n\t\t\t}\n\n\t\t\tjson.NewEncoder(w).Encode(agents)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// Simulate pagination loop\n\t\tvar agents []buildkite.Agent\n\t\tpage := 1\n\t\tlimit := 100\n\t\tperPage := 30\n\t\tvar previousFirstAgentID string\n\n\t\tfor len(agents) < limit {\n\t\t\topts := &buildkite.AgentListOptions{\n\t\t\t\tListOptions: buildkite.ListOptions{\n\t\t\t\t\tPage:    page,\n\t\t\t\t\tPerPage: perPage,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tpageAgents, _, err := client.Agents.List(context.Background(), \"test-org\", opts)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif len(pageAgents) == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif page > 1 && len(pageAgents) > 0 && pageAgents[0].ID == previousFirstAgentID {\n\t\t\t\tt.Fatal(\"unexpected duplicate page\")\n\t\t\t}\n\t\t\tif len(pageAgents) > 0 {\n\t\t\t\tpreviousFirstAgentID = pageAgents[0].ID\n\t\t\t}\n\n\t\t\tagents = append(agents, pageAgents...)\n\n\t\t\tif len(pageAgents) < perPage {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpage++\n\t\t}\n\n\t\t// Should have fetched 70 agents (30 + 30 + 10)\n\t\tif len(agents) != 70 {\n\t\t\tt.Errorf(\"expected 70 agents, got %d\", len(agents))\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "cmd/agent/pause.go",
    "content": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype PauseCmd struct {\n\tAgentID          string `arg:\"\" help:\"Agent ID to pause\"`\n\tNote             string `help:\"A descriptive note to record why the agent is paused\"`\n\tTimeoutInMinutes int    `help:\"Timeout after which the agent is automatically resumed, in minutes\" default:\"5\"`\n}\n\nfunc (c *PauseCmd) Help() string {\n\treturn `When an agent is paused, it will stop accepting new jobs but will continue\nrunning any jobs it has already started. You can optionally provide a note\nexplaining why the agent is being paused and set a timeout for automatic resumption.\n\nThe timeout must be between 1 and 1440 minutes (24 hours). If no timeout is\nspecified, the agent will pause for 5 minutes by default.\n\nExamples:\n  # Pause an agent for 5 minutes (default)\n  $ bk agent pause 0198d108-a532-4a62-9bd7-b2e744bf5c45\n\n  # Pause an agent with a note\n  $ bk agent pause 0198d108-a532-4a62-9bd7-b2e744bf5c45 --note \"Maintenance scheduled\"\n\n  # Pause an agent with a note and 60 minute timeout\n  $ bk agent pause 0198d108-a532-4a62-9bd7-b2e744bf5c45 --note \"too many llamas\" --timeout-in-minutes 60\n\n  # Pause for a short time (15 minutes) during deployment\n  $ bk agent pause 0198d108-a532-4a62-9bd7-b2e744bf5c45 --note \"Deploy in progress\" --timeout-in-minutes 15`\n}\n\nfunc (c *PauseCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\tif c.TimeoutInMinutes <= 0 {\n\t\treturn fmt.Errorf(\"timeout-in-minutes must be 1 or more\")\n\t}\n\tif c.TimeoutInMinutes > 1440 {\n\t\treturn fmt.Errorf(\"timeout-in-minutes cannot exceed 1440 minutes (1 day)\")\n\t}\n\n\tvar pauseOpts *buildkite.AgentPauseOptions\n\tif c.Note != \"\" || c.TimeoutInMinutes > 0 {\n\t\tpauseOpts = &buildkite.AgentPauseOptions{\n\t\t\tNote:             c.Note,\n\t\t\tTimeoutInMinutes: c.TimeoutInMinutes,\n\t\t}\n\t}\n\n\t_, err = f.RestAPIClient.Agents.Pause(ctx, f.Config.OrganizationSlug(), c.AgentID, pauseOpts)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to pause agent: %w\", err)\n\t}\n\n\tmessage := fmt.Sprintf(\"Agent %s paused successfully\", c.AgentID)\n\tif c.Note != \"\" {\n\t\tmessage += fmt.Sprintf(\" with note: %s\", c.Note)\n\t}\n\tif c.TimeoutInMinutes > 0 {\n\t\tmessage += fmt.Sprintf(\" (auto-resume in %d minutes)\", c.TimeoutInMinutes)\n\t}\n\n\tfmt.Printf(\"%s\\n\", message)\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/agent/pause_test.go",
    "content": "package agent\n\nimport (\n\t\"testing\"\n)\n\nfunc TestPauseCmdValidation(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname    string\n\t\ttimeout int\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t{\"valid timeout\", 60, false, \"\"},\n\t\t{\"minimum valid timeout\", 1, false, \"\"},\n\t\t{\"maximum valid timeout\", 1440, false, \"\"},\n\t\t{\"zero timeout invalid\", 0, true, \"timeout-in-minutes must be 1 or more\"},\n\t\t{\"negative timeout invalid\", -1, true, \"timeout-in-minutes must be 1 or more\"},\n\t\t{\"excessive timeout invalid\", 1441, true, \"timeout-in-minutes cannot exceed 1440 minutes (1 day)\"},\n\t\t{\"very large timeout invalid\", 10000, true, \"timeout-in-minutes cannot exceed 1440 minutes (1 day)\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tcmd := &PauseCmd{\n\t\t\t\tTimeoutInMinutes: tt.timeout,\n\t\t\t}\n\n\t\t\tvar err error\n\t\t\tif cmd.TimeoutInMinutes <= 0 {\n\t\t\t\terr = errValidation(\"timeout-in-minutes must be 1 or more\")\n\t\t\t} else if cmd.TimeoutInMinutes > 1440 {\n\t\t\t\terr = errValidation(\"timeout-in-minutes cannot exceed 1440 minutes (1 day)\")\n\t\t\t}\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"expected error but got none\")\n\t\t\t\t} else if err.Error() != tt.errMsg {\n\t\t\t\t\tt.Errorf(\"expected error %q, got %q\", tt.errMsg, err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype validationError string\n\nfunc (e validationError) Error() string { return string(e) }\nfunc errValidation(msg string) error    { return validationError(msg) }\n"
  },
  {
    "path": "cmd/agent/resume.go",
    "content": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n)\n\ntype ResumeCmd struct {\n\tAgentID string `arg:\"\" help:\"Agent ID to resume\"`\n}\n\nfunc (c *ResumeCmd) Help() string {\n\treturn `Resume a paused Buildkite agent.\n\nWhen an agent is resumed, it will start accepting new jobs again.\n\nExamples:\n  # Resume an agent\n  $ bk agent resume 0198d108-a532-4a62-9bd7-b2e744bf5c45`\n}\n\nfunc (c *ResumeCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\t_, err = f.RestAPIClient.Agents.Resume(ctx, f.Config.OrganizationSlug(), c.AgentID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to resume agent: %w\", err)\n\t}\n\n\tfmt.Printf(\"Agent %s resumed successfully\\n\", c.AgentID)\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/agent/resume_test.go",
    "content": "package agent\n\nimport (\n\t\"testing\"\n)\n\nfunc TestResumeCmdStructure(t *testing.T) {\n\tt.Parallel()\n\n\tcmd := &ResumeCmd{\n\t\tAgentID: \"test-agent-123\",\n\t}\n\n\tif cmd.AgentID != \"test-agent-123\" {\n\t\tt.Errorf(\"expected AgentID to be %q, got %q\", \"test-agent-123\", cmd.AgentID)\n\t}\n}\n\nfunc TestResumeCmdHelp(t *testing.T) {\n\tt.Parallel()\n\n\tcmd := &ResumeCmd{}\n\thelp := cmd.Help()\n\n\tif help == \"\" {\n\t\tt.Error(\"Help() should return non-empty string\")\n\t}\n\n\tif len(help) < 10 {\n\t\tt.Errorf(\"Help text seems too short: %q\", help)\n\t}\n}\n"
  },
  {
    "path": "cmd/agent/run.go",
    "content": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\n\tbkAgent \"github.com/buildkite/cli/v3/internal/agent\"\n)\n\n// RunCmd spins up an ephemeral buildkite-agent attached to a cluster.\ntype RunCmd struct {\n\tVersion     string `help:\"Specify an agent version to run\" default:\"latest\"`\n\tClusterUUID string `help:\"Cluster UUID to create the agent token on (default: the \\\"Default\\\" cluster)\" name:\"cluster-uuid\" optional:\"\"`\n\tQueue       string `help:\"Queue for the agent to listen on\" default:\"default\"`\n}\n\nfunc (r *RunCmd) Help() string {\n\treturn `Run an ephemeral buildkite-agent locally.\n\nDownloads the agent binary, creates a cluster token, and starts the agent.\nAll temporary files are cleaned up when the agent is stopped with Ctrl+C.\n\nExamples:\n  # Run the latest agent on the Default cluster\n  $ bk agent run\n\n  # Run a specific version\n  $ bk agent run --version \"3.112.0\"\n\n  # Run on a specific cluster\n  $ bk agent run --cluster-uuid \"01234567-89ab-cdef-0123-456789abcdef\"\n\n  # Run on a specific queue\n  $ bk agent run --queue \"deploy\"\n`\n}\n\nfunc (r *RunCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\t// Track temp directory for cleanup\n\ttmpDir, err := os.MkdirTemp(\"\", \"bk-agent-run-*\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating temp directory: %w\", err)\n\t}\n\tdefer func() {\n\t\tfmt.Println(\"Cleaning up temporary files...\")\n\t\tos.RemoveAll(tmpDir)\n\t}()\n\n\ttargetOS := runtime.GOOS\n\ttargetArch := runtime.GOARCH\n\n\tversion := r.Version\n\tif version == \"latest\" {\n\t\tresolved, err := bkAgent.ResolveLatestVersion()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"resolving latest version: %w\", err)\n\t\t}\n\t\tversion = resolved\n\t}\n\tversion = strings.TrimPrefix(version, \"v\")\n\n\tdownloadURL := bkAgent.BuildDownloadURL(version, targetOS, targetArch)\n\tfmt.Printf(\"Downloading buildkite-agent v%s for %s/%s...\\n\", version, targetOS, targetArch)\n\n\ttmpFile, err := bkAgent.DownloadToTemp(downloadURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"downloading agent: %w\", err)\n\t}\n\tdefer os.Remove(tmpFile)\n\n\tfmt.Println(\"Verifying checksum...\")\n\tsumsURL := bkAgent.BuildSHA256SumsURL(version)\n\tarchiveFilename := filepath.Base(downloadURL)\n\texpectedHash, err := bkAgent.FetchExpectedSHA256(sumsURL, archiveFilename)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"fetching checksum: %w\", err)\n\t}\n\tif err := bkAgent.VerifySHA256(tmpFile, expectedHash); err != nil {\n\t\treturn fmt.Errorf(\"checksum verification failed: %w\", err)\n\t}\n\n\tif err := bkAgent.ExtractBinary(tmpFile, tmpDir, targetOS); err != nil {\n\t\treturn fmt.Errorf(\"extracting agent: %w\", err)\n\t}\n\n\tbinaryPath := filepath.Join(tmpDir, bkAgent.BinaryName(targetOS))\n\n\t// Create API client and provision a cluster token\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"initializing API client: %w\", err)\n\t}\n\n\tctx := context.Background()\n\torg := f.Config.OrganizationSlug()\n\n\tclusterID, err := bkAgent.FindCluster(ctx, f, org, r.ClusterUUID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"finding cluster: %w\", err)\n\t}\n\n\tfmt.Println(\"Creating agent token...\")\n\ttoken, err := bkAgent.CreateAgentToken(ctx, f, org, clusterID, \"Ephemeral token created by bk agent run\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating agent token: %w\", err)\n\t}\n\n\t// Write a temporary config file\n\tconfigPath := filepath.Join(tmpDir, \"buildkite-agent.cfg\")\n\tbuildPath := filepath.Join(tmpDir, \"builds\")\n\tvar tags []string\n\tif r.Queue != \"\" {\n\t\ttags = append(tags, \"queue=\"+r.Queue)\n\t}\n\tif err := bkAgent.WriteAgentConfig(configPath, token, buildPath, tags); err != nil {\n\t\treturn fmt.Errorf(\"writing agent config: %w\", err)\n\t}\n\n\t// Catch signals so we wait for the agent to shut down gracefully\n\tsigCh := make(chan os.Signal, 1)\n\tsignal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)\n\n\tfmt.Printf(\"Starting buildkite-agent v%s...\\n\", version)\n\tcmd := exec.Command(binaryPath, \"start\", \"--config\", configPath)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdin = os.Stdin\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"starting agent: %w\", err)\n\t}\n\n\t// Wait for the agent to exit in the background\n\terrCh := make(chan error, 1)\n\tgo func() {\n\t\terrCh <- cmd.Wait()\n\t}()\n\n\tselect {\n\tcase <-sigCh:\n\t\tfmt.Println(\"\\nShutting down agent...\")\n\t\t// The agent already received the signal (same process group)\n\t\t// and will finish any running job before exiting.\n\t\t<-errCh\n\t\tfmt.Println(\"Agent stopped.\")\n\t\treturn nil\n\n\tcase err := <-errCh:\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"agent exited with error: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "cmd/agent/stop.go",
    "content": "package agent\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/mattn/go-isatty\"\n)\n\ntype StopCmd struct {\n\tAgents []string `arg:\"\" optional:\"\" help:\"Agent IDs to stop\"`\n\tForce  bool     `help:\"Force stop the agent. Terminating any jobs in progress\"`\n\tLimit  int64    `help:\"Limit parallel API requests\" short:\"l\" default:\"5\"`\n}\n\nfunc (c *StopCmd) Help() string {\n\treturn `Instruct one or more agents to stop accepting new build jobs and shut itself down.\nAgents can be supplied as positional arguments or from STDIN, one per line.\n\nIf the \"ORGANIZATION_SLUG/\" portion of the \"ORGANIZATION_SLUG/UUID\" agent argument\nis omitted, it uses the currently selected organization.\n\nThe --force flag applies to all agents that are stopped.\n\nExamples:\n  # Stop a single agent\n  $ bk agent stop 0198d108-a532-4a62-9bd7-b2e744bf5c45\n\n  # Stop multiple agents\n  $ bk agent stop agent-1 agent-2 agent-3\n\n  # Force stop an agent\n  $ bk agent stop 0198d108-a532-4a62-9bd7-b2e744bf5c45 --force\n\n  # Stop agents from STDIN\n  $ cat agent-ids.txt | bk agent stop`\n}\n\nfunc (c *StopCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tsigCh := make(chan os.Signal, 1)\n\tsignal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)\n\tdefer signal.Stop(sigCh)\n\n\tgo func() {\n\t\tselect {\n\t\tcase <-sigCh:\n\t\t\tcancel()\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\t}\n\t}()\n\n\tlimit := max(c.Limit, 1)\n\n\tvar agentIDs []string\n\t// this command accepts either input from stdin or positional arguments (not both) in that order\n\t// so we need to check if stdin has data for us to read and read that, otherwise use positional args and if\n\t// there are none, then we need to error\n\t// if stdin has data available, use that\n\tif bkIO.HasDataAvailable(os.Stdin) {\n\t\tscanner := bufio.NewScanner(os.Stdin)\n\t\tscanner.Split(bufio.ScanLines)\n\t\tfor scanner.Scan() {\n\t\t\tid := scanner.Text()\n\t\t\tif strings.TrimSpace(id) != \"\" {\n\t\t\t\tagentIDs = append(agentIDs, id)\n\t\t\t}\n\t\t}\n\n\t\tif scanner.Err() != nil {\n\t\t\treturn scanner.Err()\n\t\t}\n\t} else if len(c.Agents) > 0 {\n\t\tfor _, id := range c.Agents {\n\t\t\tif strings.TrimSpace(id) != \"\" {\n\t\t\t\tagentIDs = append(agentIDs, id)\n\t\t\t}\n\t\t}\n\t} else {\n\t\treturn errors.New(\"must supply agents to stop\")\n\t}\n\n\tif len(agentIDs) == 0 {\n\t\treturn errors.New(\"must supply agents to stop\")\n\t}\n\n\twriter := os.Stdout\n\tisTTY := isatty.IsTerminal(writer.Fd())\n\n\ttotal := len(agentIDs)\n\tlabel := \"Stopping agents\"\n\tif total == 1 {\n\t\tlabel = \"Stopping agent\"\n\t}\n\n\tworkerCount := int(min(limit, int64(total)))\n\n\twork := make(chan string, workerCount)\n\tupdates := make(chan stopResult, workerCount)\n\n\tvar wg sync.WaitGroup\n\tfor i := 0; i < workerCount; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor agentID := range work {\n\t\t\t\tif ctx.Err() != nil {\n\t\t\t\t\tupdates <- stopResult{id: agentID, err: ctx.Err()}\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tupdates <- stopAgent(ctx, agentID, f, c.Force)\n\t\t\t}\n\t\t}()\n\t}\n\n\tgo func() {\n\t\tfor _, id := range agentIDs {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tclose(work)\n\t\t\t\treturn\n\t\t\tcase work <- id:\n\t\t\t}\n\t\t}\n\t\tclose(work)\n\t}()\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(updates)\n\t}()\n\n\tsucceeded := 0\n\tfailed := 0\n\tcompleted := 0\n\tvar errorDetails []string\n\n\tif !f.Quiet {\n\t\tline := bkIO.ProgressLine(label, completed, total, succeeded, failed, 24)\n\t\tif isTTY {\n\t\t\tfmt.Fprint(writer, line)\n\t\t} else {\n\t\t\tfmt.Fprintln(writer, line)\n\t\t}\n\t}\n\n\tfor update := range updates {\n\t\tcompleted++\n\t\tif update.err != nil {\n\t\t\tfailed++\n\t\t\terrorDetails = append(errorDetails, fmt.Sprintf(\"FAILED %s: %v\", update.id, update.err))\n\t\t} else {\n\t\t\tsucceeded++\n\t\t}\n\n\t\tif !f.Quiet {\n\t\t\tline := bkIO.ProgressLine(label, completed, total, succeeded, failed, 24)\n\t\t\tif isTTY {\n\t\t\t\tfmt.Fprintf(writer, \"\\r%s\", line)\n\t\t\t} else {\n\t\t\t\tfmt.Fprintln(writer, line)\n\t\t\t}\n\t\t}\n\t}\n\n\tif !f.Quiet && isTTY {\n\t\tfmt.Fprintln(writer)\n\t}\n\n\tsummaryWriter := writer\n\tif failed > 0 {\n\t\tsummaryWriter = os.Stderr\n\t}\n\n\tif len(errorDetails) > 0 {\n\t\tfmt.Fprintln(summaryWriter)\n\t\tfor _, detail := range errorDetails {\n\t\t\tfmt.Fprintln(summaryWriter, detail)\n\t\t}\n\t}\n\n\tif !f.Quiet {\n\t\tagentLabel := pluralize(\"agent\", total)\n\t\tfailedLabel := pluralize(\"agent\", failed)\n\t\tif failed > 0 {\n\t\t\tfmt.Fprintf(summaryWriter, \"\\nStopped %d of %d %s (%d %s failed)\\n\", succeeded, total, agentLabel, failed, failedLabel)\n\t\t} else {\n\t\t\tfmt.Fprintf(summaryWriter, \"\\nSuccessfully stopped %d of %d %s\\n\", succeeded, total, agentLabel)\n\t\t}\n\t}\n\n\tif failed > 0 {\n\t\treturn fmt.Errorf(\"failed to stop %d of %d %s (see above for details)\", failed, total, pluralize(\"agent\", total))\n\t}\n\n\treturn nil\n}\n\ntype stopResult struct {\n\tid  string\n\terr error\n}\n\nfunc pluralize(word string, count int) string {\n\tif count == 1 {\n\t\treturn word\n\t}\n\treturn word + \"s\"\n}\n\nfunc stopAgent(ctx context.Context, id string, f *factory.Factory, force bool) stopResult {\n\torg, agentID := parseAgentArg(id, f.Config)\n\t_, err := f.RestAPIClient.Agents.Stop(ctx, org, agentID, force)\n\treturn stopResult{id: id, err: err}\n}\n"
  },
  {
    "path": "cmd/agent/stop_test.go",
    "content": "package agent\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc TestStopCmdStructure(t *testing.T) {\n\tt.Parallel()\n\n\tcmd := &StopCmd{\n\t\tAgents: []string{\"agent-1\", \"agent-2\"},\n\t\tLimit:  5,\n\t\tForce:  true,\n\t}\n\n\tif len(cmd.Agents) != 2 {\n\t\tt.Errorf(\"expected 2 agents, got %d\", len(cmd.Agents))\n\t}\n\n\tif cmd.Limit != 5 {\n\t\tt.Errorf(\"expected Limit to be 5, got %d\", cmd.Limit)\n\t}\n\n\tif !cmd.Force {\n\t\tt.Error(\"expected Force to be true\")\n\t}\n}\n\nfunc TestStopCmdHelp(t *testing.T) {\n\tt.Parallel()\n\n\tcmd := &StopCmd{}\n\thelp := cmd.Help()\n\n\tif help == \"\" {\n\t\tt.Error(\"Help() should return non-empty string\")\n\t}\n\n\tif !strings.Contains(strings.ToLower(help), \"agent\") {\n\t\tt.Error(\"Help text should mention agents\")\n\t}\n}\n\nfunc TestStopAgentErrorCollection(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"parses agent arg correctly\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\tconf.SelectOrganization(\"default-org\", false)\n\n\t\ttests := []struct {\n\t\t\tname        string\n\t\t\tinput       string\n\t\t\texpectedOrg string\n\t\t\texpectedID  string\n\t\t}{\n\t\t\t{\n\t\t\t\tname:        \"agent ID only\",\n\t\t\t\tinput:       \"agent-123\",\n\t\t\t\texpectedOrg: \"default-org\",\n\t\t\t\texpectedID:  \"agent-123\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:        \"org/agent format\",\n\t\t\t\tinput:       \"custom-org/agent-456\",\n\t\t\t\texpectedOrg: \"custom-org\",\n\t\t\t\texpectedID:  \"agent-456\",\n\t\t\t},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t\torg, id := parseAgentArg(tt.input, conf)\n\n\t\t\t\tif org != tt.expectedOrg {\n\t\t\t\t\tt.Errorf(\"expected org %q, got %q\", tt.expectedOrg, org)\n\t\t\t\t}\n\n\t\t\t\tif id != tt.expectedID {\n\t\t\t\t\tt.Errorf(\"expected id %q, got %q\", tt.expectedID, id)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc TestStopAgentBulkOperationErrorHandling(t *testing.T) {\n\tt.Parallel()\n\n\t// This test verifies the error collection logic without running the full command\n\tt.Run(\"error details format\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\terrorDetails := []string{}\n\n\t\t// Simulate collecting errors\n\t\tupdates := []stopResult{\n\t\t\t{id: \"agent-1\", err: nil},\n\t\t\t{id: \"agent-2\", err: fmt.Errorf(\"connection timeout\")},\n\t\t\t{id: \"agent-3\", err: nil},\n\t\t\t{id: \"agent-4\", err: fmt.Errorf(\"not found\")},\n\t\t}\n\n\t\tfor _, update := range updates {\n\t\t\tif update.err != nil {\n\t\t\t\terrorDetails = append(errorDetails, fmt.Sprintf(\"FAILED %s: %v\", update.id, update.err))\n\t\t\t}\n\t\t}\n\n\t\tif len(errorDetails) != 2 {\n\t\t\tt.Errorf(\"expected 2 error details, got %d\", len(errorDetails))\n\t\t}\n\n\t\tif !strings.Contains(errorDetails[0], \"agent-2\") {\n\t\t\tt.Error(\"expected first error to mention agent-2\")\n\t\t}\n\n\t\tif !strings.Contains(errorDetails[1], \"agent-4\") {\n\t\t\tt.Error(\"expected second error to mention agent-4\")\n\t\t}\n\n\t\tif !strings.Contains(errorDetails[0], \"connection timeout\") {\n\t\t\tt.Error(\"expected first error to include 'connection timeout'\")\n\t\t}\n\n\t\tif !strings.Contains(errorDetails[1], \"not found\") {\n\t\t\tt.Error(\"expected second error to include 'not found'\")\n\t\t}\n\t})\n\n\tt.Run(\"progress tracking\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttotal := 10\n\t\tsucceeded := 0\n\t\tfailed := 0\n\t\tcompleted := 0\n\n\t\tupdates := []stopResult{\n\t\t\t{id: \"agent-1\", err: nil},\n\t\t\t{id: \"agent-2\", err: nil},\n\t\t\t{id: \"agent-3\", err: fmt.Errorf(\"timeout\")},\n\t\t\t{id: \"agent-4\", err: nil},\n\t\t\t{id: \"agent-5\", err: fmt.Errorf(\"not found\")},\n\t\t}\n\n\t\tfor _, update := range updates {\n\t\t\tcompleted++\n\t\t\tif update.err != nil {\n\t\t\t\tfailed++\n\t\t\t} else {\n\t\t\t\tsucceeded++\n\t\t\t}\n\t\t}\n\n\t\tif completed != 5 {\n\t\t\tt.Errorf(\"expected completed=5, got %d\", completed)\n\t\t}\n\n\t\tif succeeded != 3 {\n\t\t\tt.Errorf(\"expected succeeded=3, got %d\", succeeded)\n\t\t}\n\n\t\tif failed != 2 {\n\t\t\tt.Errorf(\"expected failed=2, got %d\", failed)\n\t\t}\n\n\t\texpectedPercent := (completed * 100) / total\n\t\tif expectedPercent != 50 {\n\t\t\tt.Errorf(\"expected 50%% progress, got %d%%\", expectedPercent)\n\t\t}\n\t})\n}\n\nfunc TestStopProgressOutput(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"progress line format\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tline := bkIO.ProgressLine(\"Stopping agents\", 5, 10, 3, 2, 6)\n\n\t\tif !strings.Contains(line, \"Stopping agents\") {\n\t\t\tt.Error(\"expected line to contain 'Stopping agents'\")\n\t\t}\n\t\tif !strings.Contains(line, \"50%\") {\n\t\t\tt.Error(\"expected line to contain percentage\")\n\t\t}\n\t\tif !strings.Contains(line, \"5/10\") {\n\t\t\tt.Error(\"expected line to contain completed/total\")\n\t\t}\n\t\tif !strings.Contains(line, \"succeeded:3\") {\n\t\t\tt.Error(\"expected line to contain success count\")\n\t\t}\n\t\tif !strings.Contains(line, \"failed:2\") {\n\t\t\tt.Error(\"expected line to contain fail count\")\n\t\t}\n\t\tif !strings.Contains(line, \"[\") || !strings.Contains(line, \"]\") {\n\t\t\tt.Error(\"expected line to contain progress bar brackets\")\n\t\t}\n\t})\n}\n\nfunc TestPluralize(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tcount int\n\t\twant  string\n\t}{\n\t\t{count: 1, want: \"agent\"},\n\t\t{count: 0, want: \"agents\"},\n\t\t{count: 2, want: \"agents\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tt.Run(fmt.Sprintf(\"count_%d\", tt.count), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif got := pluralize(\"agent\", tt.count); got != tt.want {\n\t\t\t\tt.Fatalf(\"pluralize() = %q, want %q\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/agent/util.go",
    "content": "package agent\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n)\n\nfunc parseAgentArg(agent string, conf *config.Config) (string, string) {\n\tvar org, id string\n\tagentIsURL := strings.Contains(agent, \":\")\n\tagentIsSlug := !agentIsURL && strings.Contains(agent, \"/\")\n\n\tif agentIsURL {\n\t\turl, err := url.Parse(agent)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\"\n\t\t}\n\t\tpart := strings.Split(url.Path, \"/\")\n\t\tif part[3] == \"agents\" {\n\t\t\torg, id = part[2], part[4]\n\t\t} else {\n\t\t\torg, id = part[2], part[len(part)-1]\n\t\t}\n\t} else {\n\t\tif agentIsSlug {\n\t\t\tpart := strings.Split(agent, \"/\")\n\t\t\torg, id = part[0], part[1]\n\t\t} else {\n\t\t\torg = conf.OrganizationSlug()\n\t\t\tid = agent\n\t\t}\n\t}\n\n\treturn org, id\n}\n"
  },
  {
    "path": "cmd/agent/view.go",
    "content": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\t\"github.com/pkg/browser\"\n)\n\ntype ViewCmd struct {\n\tAgent string `arg:\"\" help:\"Agent ID to view\"`\n\tWeb   bool   `help:\"Open agent in a browser\" short:\"w\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ViewCmd) Help() string {\n\treturn `If the \"ORGANIZATION_SLUG/\" portion of the \"ORGANIZATION_SLUG/UUID\" agent argument\nis omitted, it uses the currently selected organization.\n\nExamples:\n  # View an agent\n  $ bk agent view 0198d108-a532-4a62-9bd7-b2e744bf5c45\n\n  # View an agent with organization slug\n  $ bk agent view my-org/0198d108-a532-4a62-9bd7-b2e744bf5c45\n\n  # Open agent in browser\n  $ bk agent view 0198d108-a532-4a62-9bd7-b2e744bf5c45 --web\n\n  # View agent as JSON\n  $ bk agent view 0198d108-a532-4a62-9bd7-b2e744bf5c45 --output json`\n}\n\nfunc (c *ViewCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\torg, id := parseAgentArg(c.Agent, f.Config)\n\n\tif c.Web {\n\t\turl := fmt.Sprintf(\"https://buildkite.com/organizations/%s/agents/%s\", org, id)\n\t\tfmt.Printf(\"Opening %s in your browser\\n\", url)\n\t\treturn browser.OpenURL(url)\n\t}\n\n\tvar agentData buildkite.Agent\n\tif err = bkIO.SpinWhile(f, \"Loading agent\", func() error {\n\t\tvar apiErr error\n\t\tagentData, _, apiErr = f.RestAPIClient.Agents.Get(ctx, org, id)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, agentData, format)\n\t}\n\n\tmetadata, queue := parseMetadata(agentData.Metadata)\n\tif metadata == \"\" {\n\t\tmetadata = \"~\"\n\t}\n\tconnected := \"-\"\n\tif agentData.CreatedAt != nil {\n\t\tconnected = agentData.CreatedAt.Format(time.RFC3339)\n\t}\n\n\theaders := []string{\"Property\", \"Value\"}\n\trows := [][]string{\n\t\t{\"ID\", agentData.ID},\n\t\t{\"Name\", agentData.Name},\n\t\t{\"State\", agentData.ConnectedState},\n\t\t{\"Queue\", queue},\n\t\t{\"Version\", agentData.Version},\n\t\t{\"Hostname\", agentData.Hostname},\n\t\t{\"User Agent\", agentData.UserAgent},\n\t\t{\"IP Address\", agentData.IPAddress},\n\t\t{\"Connected\", connected},\n\t\t{\"Metadata\", metadata},\n\t}\n\n\ttable := output.Table(headers, rows, map[string]string{\n\t\t\"property\": \"bold\",\n\t\t\"value\":    \"dim\",\n\t})\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\tfmt.Fprintf(writer, \"Agent %s (%s)\\n\\n\", agentData.Name, agentData.ID)\n\tfmt.Fprint(writer, table)\n\n\treturn nil\n}\n\nfunc parseMetadata(metadataList []string) (string, string) {\n\tvar metadataTags []string\n\tvar queue string\n\n\tif len(metadataList) == 1 {\n\t\tif queueValue := parseQueue(metadataList[0]); queueValue != \"\" {\n\t\t\treturn \"~\", queueValue\n\t\t}\n\t\treturn metadataList[0], \"default\"\n\t}\n\n\tfor _, v := range metadataList {\n\t\tif queueValue := parseQueue(v); queueValue != \"\" {\n\t\t\tqueue = queueValue\n\t\t} else {\n\t\t\tmetadataTags = append(metadataTags, v)\n\t\t}\n\t}\n\n\tif queue == \"\" {\n\t\tqueue = \"default\"\n\t}\n\n\tmetadata := strings.Join(metadataTags, \", \")\n\treturn metadata, queue\n}\n\nfunc parseQueue(metadata string) string {\n\tparts := strings.Split(metadata, \"=\")\n\tif len(parts) > 1 && parts[0] == \"queue\" {\n\t\treturn parts[1]\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "cmd/agent/view_test.go",
    "content": "package agent\n\nimport \"testing\"\n\nfunc TestParseMetadata(t *testing.T) {\n\tcases := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\tmetadata string\n\t\tqueue    string\n\t}{\n\t\t{\n\t\t\tname:     \"single queue entry\",\n\t\t\tinput:    []string{\"queue=production\"},\n\t\t\tmetadata: \"~\",\n\t\t\tqueue:    \"production\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single non-queue entry\",\n\t\t\tinput:    []string{\"os=linux\"},\n\t\t\tmetadata: \"os=linux\",\n\t\t\tqueue:    \"default\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple entries with queue\",\n\t\t\tinput:    []string{\"queue=deploy\", \"os=linux\", \"region=us\"},\n\t\t\tmetadata: \"os=linux, region=us\",\n\t\t\tqueue:    \"deploy\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no entries\",\n\t\t\tinput:    nil,\n\t\t\tmetadata: \"\",\n\t\t\tqueue:    \"default\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple entries without queue\",\n\t\t\tinput:    []string{\"os=linux\", \"region=us\"},\n\t\t\tmetadata: \"os=linux, region=us\",\n\t\t\tqueue:    \"default\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmetadata, queue := parseMetadata(tc.input)\n\t\t\tif metadata != tc.metadata {\n\t\t\t\tt.Fatalf(\"metadata mismatch: got %q want %q\", metadata, tc.metadata)\n\t\t\t}\n\t\t\tif queue != tc.queue {\n\t\t\t\tt.Fatalf(\"queue mismatch: got %q want %q\", queue, tc.queue)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/api/api.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Khan/genqlient/graphql\"\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\thttpClient \"github.com/buildkite/cli/v3/internal/http\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/vektah/gqlparser/v2/ast\"\n\t\"github.com/vektah/gqlparser/v2/parser\"\n)\n\ntype ApiCmd struct {\n\tEndpoint  string   `arg:\"\" optional:\"\" help:\"API endpoint to call\"`\n\tMethod    string   `help:\"HTTP method to use\" short:\"X\"`\n\tHeaders   []string `help:\"Headers to include in the request\" short:\"H\"`\n\tData      string   `help:\"Data to send in the request body\" short:\"d\"`\n\tAnalytics bool     `help:\"Use the Test Analytics endpoint\"`\n\tFile      string   `help:\"File containing GraphQL query\" short:\"f\"`\n\tVerbose   bool     `help:\"Enable verbose output (currently only provides information about rate limit exceeded retries)\"`\n}\n\nfunc (c *ApiCmd) Help() string {\n\treturn `\nInteract with either the REST or GraphQL Buildkite APIs.\n\nExamples:\n  # To get a build\n  $ bk api /pipelines/example-pipeline/builds/420\n\n  # To create a pipeline\n  $ bk api --method POST /pipelines --data '\n  {\n    \"name\": \"My Cool Pipeline\",\n    \"repository\": \"git@github.com:acme-inc/my-pipeline.git\",\n    \"configuration\": \"steps:\\n - command: env\"\n  }\n  '\n\n  # To update a cluster\n  $ bk api --method PUT /clusters/CLUSTER_UUID --data '\n  {\n    \"name\": \"My Updated Cluster\",\n  }\n  '\n\n  # To get all test suites\n  $ bk api --analytics /suites\n\n  # Run GraphQL query from file\n  $ bk api --file get_build.graphql\n`\n}\n\n// buildFullEndpoint constructs the full API endpoint path with organization prefix\nfunc buildFullEndpoint(endpoint, orgSlug string, isAnalytics bool) string {\n\t// Default to root if empty\n\tif endpoint == \"\" {\n\t\tendpoint = \"/\"\n\t}\n\n\t// Ensure endpoint starts with a leading slash\n\tif !strings.HasPrefix(endpoint, \"/\") {\n\t\tendpoint = \"/\" + endpoint\n\t}\n\n\tvar endpointPrefix string\n\tif isAnalytics {\n\t\tendpointPrefix = fmt.Sprintf(\"v2/analytics/organizations/%s\", orgSlug)\n\t} else {\n\t\tendpointPrefix = fmt.Sprintf(\"v2/organizations/%s\", orgSlug)\n\t}\n\n\treturn endpointPrefix + endpoint\n}\n\nfunc (c *ApiCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\t// Determine HTTP method: default to GET, but use POST if data is provided and method not explicitly set\n\tmethod := c.Method\n\tif method == \"\" {\n\t\tif c.Data != \"\" {\n\t\t\tmethod = \"POST\"\n\t\t} else {\n\t\t\tmethod = \"GET\"\n\t\t}\n\t}\n\n\t// Handle GraphQL file queries\n\tif c.File != \"\" {\n\t\treturn c.handleGraphQLQuery(context.Background(), f)\n\t}\n\n\tfullEndpoint := buildFullEndpoint(c.Endpoint, f.Config.OrganizationSlug(), c.Analytics)\n\n\t// Create an HTTP client with rate-limit retry via the shared transport.\n\trl := httpClient.NewRateLimitTransport(nil)\n\trl.MaxRetryDelay = 60 * time.Second\n\trl.OnRateLimit = func(attempt int, delay time.Duration) {\n\t\tif c.Verbose {\n\t\t\tfmt.Fprintf(os.Stderr, \"WARNING: Rate limit exceeded, retrying in %v @ %q (attempt %d)\\n\", delay, time.Now().Add(delay).Format(time.RFC3339), attempt)\n\t\t}\n\t}\n\n\tclient := httpClient.NewClient(\n\t\tf.Config.APIToken(),\n\t\thttpClient.WithBaseURL(f.RestAPIClient.BaseURL.String()),\n\t\thttpClient.WithHTTPClient(&http.Client{Transport: rl}),\n\t)\n\n\t// Process custom headers\n\tcustomHeaders := make(map[string]string)\n\tfor _, header := range c.Headers {\n\t\tparts := strings.SplitN(header, \":\", 2)\n\t\tif len(parts) == 2 {\n\t\t\tcustomHeaders[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])\n\t\t}\n\t}\n\n\tvar requestData any\n\tif c.Data != \"\" {\n\t\t// Try to parse as JSON first\n\t\tif err := json.Unmarshal([]byte(c.Data), &requestData); err != nil {\n\t\t\t// If not JSON, use raw string\n\t\t\trequestData = c.Data\n\t\t}\n\t}\n\n\tvar response any\n\n\tswitch method {\n\tcase \"GET\":\n\t\terr = client.Get(context.Background(), fullEndpoint, &response)\n\tcase \"POST\":\n\t\terr = client.Post(context.Background(), fullEndpoint, requestData, &response)\n\tcase \"PUT\":\n\t\terr = client.Put(context.Background(), fullEndpoint, requestData, &response)\n\tdefault:\n\t\t// For other methods, use the Do method directly\n\t\terr = client.Do(context.Background(), method, fullEndpoint, requestData, &response)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error making request: %w\", err)\n\t}\n\n\t// Format and print the response\n\tvar prettyJSON bytes.Buffer\n\tresponseBytes, err := json.Marshal(response)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshaling response: %w\", err)\n\t}\n\n\terr = json.Indent(&prettyJSON, responseBytes, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error formatting JSON response: %w\", err)\n\t}\n\n\tfmt.Println(prettyJSON.String())\n\n\treturn nil\n}\n\nfunc (c *ApiCmd) handleGraphQLQuery(ctx context.Context, f *factory.Factory) error {\n\t// Read the GraphQL query from file\n\tqueryBytes, err := os.ReadFile(c.File)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading GraphQL query file %s: %w\", c.File, err)\n\t}\n\n\t// Validate and parse GraphQL query\n\tquery := strings.TrimSpace(string(queryBytes))\n\tif query == \"\" {\n\t\treturn fmt.Errorf(\"GraphQL query file %s is empty\", c.File)\n\t}\n\n\tdoc, err := parser.ParseQuery(&ast.Source{Input: query})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid GraphQL query: %w\", err)\n\t}\n\n\t// Validate that we have at least one operation\n\tif len(doc.Operations) == 0 {\n\t\treturn fmt.Errorf(\"GraphQL query must contain at least one operation (query, mutation, or subscription)\")\n\t}\n\n\t// Extract and validate operation name (Buildkite GraphQL API requires named operations)\n\topName := doc.Operations[0].Name\n\tif opName == \"\" {\n\t\treturn fmt.Errorf(\"GraphQL operation must have a name when using file input. Please add a name after the operation type, e.g., 'query MyQuery { ... }'\")\n\t}\n\n\t// Create GraphQL request using the existing client infrastructure\n\treq := &graphql.Request{\n\t\tOpName: opName,\n\t\tQuery:  query,\n\t}\n\n\t// Use a generic response type for raw queries\n\tresp := &graphql.Response{Data: new(interface{})}\n\n\t// Use the existing GraphQL client\n\tif err = f.GraphQLClient.MakeRequest(ctx, req, resp); err != nil {\n\t\treturn fmt.Errorf(\"error making GraphQL request: %w\", err)\n\t}\n\n\t// Format and print the response\n\tresponseBytes, err := json.Marshal(resp)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshaling response: %w\", err)\n\t}\n\n\tvar prettyJSON bytes.Buffer\n\tif err = json.Indent(&prettyJSON, responseBytes, \"\", \"  \"); err != nil {\n\t\treturn fmt.Errorf(\"error formatting JSON response: %w\", err)\n\t}\n\n\tfmt.Println(prettyJSON.String())\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/api/api_test.go",
    "content": "package api\n\nimport (\n\t\"testing\"\n)\n\nfunc TestBuildFullEndpoint(t *testing.T) {\n\tt.Parallel()\n\n\ttestcases := map[string]struct {\n\t\tendpoint     string\n\t\torgSlug      string\n\t\tisAnalytics  bool\n\t\twantEndpoint string\n\t}{\n\t\t\"endpoint with leading slash\": {\n\t\t\tendpoint:     \"/pipelines/dummy/builds/5085\",\n\t\t\torgSlug:      \"test-org\",\n\t\t\tisAnalytics:  false,\n\t\t\twantEndpoint: \"v2/organizations/test-org/pipelines/dummy/builds/5085\",\n\t\t},\n\t\t\"endpoint without leading slash\": {\n\t\t\tendpoint:     \"pipelines/dummy/builds/5085\",\n\t\t\torgSlug:      \"test-org\",\n\t\t\tisAnalytics:  false,\n\t\t\twantEndpoint: \"v2/organizations/test-org/pipelines/dummy/builds/5085\",\n\t\t},\n\t\t\"empty endpoint\": {\n\t\t\tendpoint:     \"\",\n\t\t\torgSlug:      \"test-org\",\n\t\t\tisAnalytics:  false,\n\t\t\twantEndpoint: \"v2/organizations/test-org/\",\n\t\t},\n\t\t\"root endpoint\": {\n\t\t\tendpoint:     \"/\",\n\t\t\torgSlug:      \"test-org\",\n\t\t\tisAnalytics:  false,\n\t\t\twantEndpoint: \"v2/organizations/test-org/\",\n\t\t},\n\t\t\"analytics endpoint with leading slash\": {\n\t\t\tendpoint:     \"/suites\",\n\t\t\torgSlug:      \"test-org\",\n\t\t\tisAnalytics:  true,\n\t\t\twantEndpoint: \"v2/analytics/organizations/test-org/suites\",\n\t\t},\n\t\t\"analytics endpoint without leading slash\": {\n\t\t\tendpoint:     \"suites\",\n\t\t\torgSlug:      \"test-org\",\n\t\t\tisAnalytics:  true,\n\t\t\twantEndpoint: \"v2/analytics/organizations/test-org/suites\",\n\t\t},\n\t\t\"pipeline endpoint without leading slash\": {\n\t\t\tendpoint:     \"pipelines\",\n\t\t\torgSlug:      \"acme-inc\",\n\t\t\tisAnalytics:  false,\n\t\t\twantEndpoint: \"v2/organizations/acme-inc/pipelines\",\n\t\t},\n\t}\n\n\tfor name, tc := range testcases {\n\t\ttc := tc\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgot := buildFullEndpoint(tc.endpoint, tc.orgSlug, tc.isAnalytics)\n\n\t\t\tif got != tc.wantEndpoint {\n\t\t\t\tt.Errorf(\"buildFullEndpoint(%q, %q, %v) = %q, want %q\",\n\t\t\t\t\ttc.endpoint, tc.orgSlug, tc.isAnalytics, got, tc.wantEndpoint)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/artifacts/download.go",
    "content": "package artifacts\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/alecthomas/kong\"\n\tbuildResolver \"github.com/buildkite/cli/v3/internal/build/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/build/resolver/options\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkErrors \"github.com/buildkite/cli/v3/internal/errors\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype DownloadCmd struct {\n\tArtifactID  string `arg:\"\" optional:\"\" help:\"Artifact ID to download. If omitted, all artifacts are downloaded. Use 'bk artifacts list' to find IDs.\"`\n\tBuildNumber string `help:\"Build number containing the artifact. If omitted, the most recent build on the current branch will be used.\" short:\"b\" name:\"build\"`\n\tPipeline    string `help:\"The pipeline containing the artifact. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}. If omitted, it will be resolved using the current directory.\" short:\"p\"`\n\tJobUUID     string `help:\"The job UUID containing the artifact.\" short:\"j\" name:\"job-uuid\"`\n}\n\nfunc (c *DownloadCmd) Help() string {\n\treturn `\nUse this command to download artifacts from a build.\n\nIf no artifact ID is provided, all artifacts for the build (or job) will be downloaded.\nArtifact IDs can be found using \"bk artifacts list\".\n\nExamples:\n  # Download all artifacts from the most recent build on the current branch\n  $ bk artifacts download\n\n  # Download all artifacts from a specific build\n  $ bk artifacts download --build 429\n\n  # Download all artifacts from a specific job\n  $ bk artifacts download --build 429 --job-uuid 0193903e-ecd9-4c51-9156-0738da987e87\n\n  # Download a specific artifact\n  $ bk artifacts download 0191727d-b5ce-4576-b37d-477ae0ca830c --build 429\n\n  # Specify the pipeline explicitly\n  $ bk artifacts download --build 429 -p monolith\n`\n}\n\nfunc (c *DownloadCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tpipelineRes := pipelineResolver.NewAggregateResolver(\n\t\tpipelineResolver.ResolveFromFlag(c.Pipeline, f.Config),\n\t\tpipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)),\n\t\tpipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOneWithFactory(f))),\n\t)\n\n\toptionsResolver := options.AggregateResolver{\n\t\toptions.ResolveBranchFromFlag(\"\"),\n\t\toptions.ResolveBranchFromRepository(f.GitRepository),\n\t}\n\n\tvar buildResolvers []buildResolver.BuildResolverFn\n\tif c.BuildNumber != \"\" {\n\t\tbuildResolvers = append(buildResolvers, buildResolver.ResolveFromPositionalArgument([]string{c.BuildNumber}, 0, pipelineRes.Resolve, f.Config))\n\t}\n\tbuildResolvers = append(buildResolvers, buildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...))\n\n\tbuildRes := buildResolver.NewAggregateResolver(buildResolvers...)\n\n\tctx := context.Background()\n\tbld, err := buildRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif bld == nil {\n\t\treturn bkErrors.NewResourceNotFoundError(nil, \"no build found\")\n\t}\n\n\tbuild := strconv.Itoa(bld.BuildNumber)\n\n\tif c.ArtifactID != \"\" {\n\t\treturn c.downloadOne(ctx, f, bld.Organization, bld.Pipeline, build)\n\t}\n\n\treturn c.downloadAll(ctx, f, bld.Organization, bld.Pipeline, build)\n}\n\nfunc (c *DownloadCmd) downloadOne(ctx context.Context, f *factory.Factory, org, pipeline, build string) error {\n\tvar filename string\n\n\tif err := bkIO.SpinWhile(f, \"Downloading artifact\", func() error {\n\t\tartifact, findErr := findArtifact(ctx, f, org, pipeline, build, c.ArtifactID, c.JobUUID)\n\t\tif findErr != nil {\n\t\t\treturn findErr\n\t\t}\n\t\tvar dlErr error\n\t\tfilename, dlErr = downloadArtifact(ctx, f, artifact)\n\t\treturn dlErr\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Printf(\"Downloaded: %s\\n\", filename)\n\treturn nil\n}\n\nfunc (c *DownloadCmd) downloadAll(ctx context.Context, f *factory.Factory, org, pipeline, build string) error {\n\tvar artifacts []buildkite.Artifact\n\n\tif err := bkIO.SpinWhile(f, \"Loading artifacts\", func() error {\n\t\tvar err error\n\t\tartifacts, err = listArtifacts(ctx, f, org, pipeline, build, c.JobUUID)\n\t\treturn err\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tif len(artifacts) == 0 {\n\t\tfmt.Println(\"No artifacts found.\")\n\t\treturn nil\n\t}\n\n\tdirectory := fmt.Sprintf(\"artifacts-build-%s\", build)\n\tif err := os.MkdirAll(directory, os.ModePerm); err != nil {\n\t\treturn err\n\t}\n\n\tfor i := range artifacts {\n\t\ta := &artifacts[i]\n\t\tdestPath := filepath.Join(directory, filepath.FromSlash(a.Path))\n\t\tif err := downloadToFile(ctx, f, a.DownloadURL, destPath); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfmt.Printf(\"Downloaded: %s\\n\", a.Path)\n\t}\n\n\tfmt.Printf(\"Downloaded %d artifacts to: %s\\n\", len(artifacts), directory)\n\treturn nil\n}\n\nfunc findArtifact(ctx context.Context, f *factory.Factory, org, pipeline, build, artifactID, jobUUID string) (*buildkite.Artifact, error) {\n\tif jobUUID != \"\" {\n\t\tartifact, _, err := f.RestAPIClient.Artifacts.Get(ctx, org, pipeline, build, jobUUID, artifactID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &artifact, nil\n\t}\n\n\tartifacts, err := listArtifacts(ctx, f, org, pipeline, build, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor i := range artifacts {\n\t\tif artifacts[i].ID == artifactID {\n\t\t\treturn &artifacts[i], nil\n\t\t}\n\t}\n\n\treturn nil, bkErrors.NewResourceNotFoundError(nil, fmt.Sprintf(\"no artifact found with ID %s in build #%s\", artifactID, build))\n}\n\n// listArtifacts fetches all artifacts for a build or job, paginating through all results.\nfunc listArtifacts(ctx context.Context, f *factory.Factory, org, pipeline, build, jobUUID string) ([]buildkite.Artifact, error) {\n\tvar all []buildkite.Artifact\n\topts := &buildkite.ArtifactListOptions{\n\t\tListOptions: buildkite.ListOptions{PerPage: 100},\n\t}\n\n\tfor {\n\t\tvar artifacts []buildkite.Artifact\n\t\tvar resp *buildkite.Response\n\t\tvar err error\n\n\t\tif jobUUID != \"\" {\n\t\t\tartifacts, resp, err = f.RestAPIClient.Artifacts.ListByJob(ctx, org, pipeline, build, jobUUID, opts)\n\t\t} else {\n\t\t\tartifacts, resp, err = f.RestAPIClient.Artifacts.ListByBuild(ctx, org, pipeline, build, opts)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tall = append(all, artifacts...)\n\n\t\tif resp.NextPage == 0 {\n\t\t\tbreak\n\t\t}\n\t\topts.Page = resp.NextPage\n\t}\n\n\treturn all, nil\n}\n\nfunc downloadArtifact(ctx context.Context, f *factory.Factory, artifact *buildkite.Artifact) (string, error) {\n\tdestPath := filepath.FromSlash(artifact.Path)\n\tif err := downloadToFile(ctx, f, artifact.DownloadURL, destPath); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn destPath, nil\n}\n\nfunc downloadToFile(ctx context.Context, f *factory.Factory, url, destPath string) error {\n\tif err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {\n\t\treturn err\n\t}\n\n\tout, err := os.Create(destPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer out.Close()\n\n\t_, err = f.RestAPIClient.Artifacts.DownloadArtifactByURL(ctx, url, out)\n\treturn err\n}\n"
  },
  {
    "path": "cmd/artifacts/list.go",
    "content": "package artifacts\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/artifact\"\n\tbuildResolver \"github.com/buildkite/cli/v3/internal/build/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/build/resolver/options\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype ListCmd struct {\n\tBuildNumber string `arg:\"\" optional:\"\" help:\"Build number to list artifacts for\"`\n\tPipeline    string `help:\"The pipeline to view. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}. If omitted, it will be resolved using the current directory.\" short:\"p\"`\n\tJobUUID     string `help:\"List artifacts for a specific job on the given build.\" short:\"j\" name:\"job-uuid\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ListCmd) Help() string {\n\treturn `\nList artifacts for a build or a job in a build.\n\nYou can pass an optional build number. If omitted, the most recent build on the current branch will be resolved.\n\nExamples:\n  # By default, artifacts of the most recent build for the current branch is shown\n  $ bk artifacts list\n\n  # To list artifacts of a specific build\n  $ bk artifacts list 429\n\n  # To list artifacts of a specific job in a build\n  $ bk artifacts list 429 --job-uuid 0193903e-ecd9-4c51-9156-0738da987e87\n\n  # If not inside a repository or to use a specific pipeline, pass -p\n  $ bk artifacts list 429 -p monolith\n`\n}\n\nfunc (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tvar args []string\n\tif c.BuildNumber != \"\" {\n\t\targs = []string{c.BuildNumber}\n\t}\n\n\t// Resolve a pipeline based on how bk build resolves the pipeline\n\tpipelineRes := pipelineResolver.NewAggregateResolver(\n\t\tpipelineResolver.ResolveFromFlag(c.Pipeline, f.Config),\n\t\tpipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)),\n\t\tpipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOneWithFactory(f))),\n\t)\n\n\t// We resolve a build an optional argument or positional argument\n\toptionsResolver := options.AggregateResolver{\n\t\toptions.ResolveBranchFromFlag(\"\"),\n\t\toptions.ResolveBranchFromRepository(f.GitRepository),\n\t}\n\n\tbuildRes := buildResolver.NewAggregateResolver(\n\t\tbuildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config),\n\t\tbuildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...),\n\t)\n\n\tctx := context.Background()\n\tbld, err := buildRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif bld == nil {\n\t\treturn output.WriteTextOrStructured(os.Stdout, format, []buildkite.Artifact{}, \"No build found.\")\n\t}\n\n\tvar buildArtifacts []buildkite.Artifact\n\n\tif err = bkIO.SpinWhile(f, \"Loading artifacts information\", func() error {\n\t\tbuildArtifacts, err = listArtifacts(ctx, f, bld.Organization, bld.Pipeline, fmt.Sprint(bld.BuildNumber), c.JobUUID)\n\t\treturn err\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, buildArtifacts, format)\n\t}\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\tif len(buildArtifacts) == 0 {\n\t\tfmt.Fprintln(writer, \"No artifacts found.\")\n\t\treturn nil\n\t}\n\n\tbuildURL := fmt.Sprintf(\"https://buildkite.com/organizations/%s/pipelines/%s/builds/%d\", bld.Organization, bld.Pipeline, bld.BuildNumber)\n\n\tif c.JobUUID != \"\" {\n\t\tjobURL := fmt.Sprintf(\"%s/jobs/%s\", buildURL, c.JobUUID)\n\t\tfmt.Fprintf(writer, \"Showing %d artifacts for %s/%s build #%d (job %s): %s\\n\\n\", len(buildArtifacts), bld.Organization, bld.Pipeline, bld.BuildNumber, c.JobUUID, jobURL)\n\t} else {\n\t\tfmt.Fprintf(writer, \"Showing %d artifacts for %s/%s build #%d: %s\\n\\n\", len(buildArtifacts), bld.Organization, bld.Pipeline, bld.BuildNumber, buildURL)\n\t}\n\n\treturn displayArtifacts(buildArtifacts, writer, buildURL)\n}\n\nfunc displayArtifacts(artifacts []buildkite.Artifact, writer io.Writer, baseBuildURL string) error {\n\theaders := []string{\"ID\", \"Path\", \"Size\", \"URL\"}\n\tvar rows [][]string\n\n\tfor _, a := range artifacts {\n\t\turl := \"-\"\n\t\tif a.JobID != \"\" {\n\t\t\turl = fmt.Sprintf(\"%s/jobs/%s/artifacts/%s\", baseBuildURL, a.JobID, a.ID)\n\t\t} else if a.URL != \"\" {\n\t\t\turl = a.URL\n\t\t}\n\t\trows = append(rows, []string{\n\t\t\ta.ID,\n\t\t\ta.Path,\n\t\t\tartifact.FormatBytes(a.FileSize),\n\t\t\turl,\n\t\t})\n\t}\n\n\ttable := output.Table(headers, rows, map[string]string{\n\t\t\"id\":   \"dim\",\n\t\t\"path\": \"bold\",\n\t\t\"size\": \"dim\",\n\t\t\"url\":  \"dim\",\n\t})\n\n\tfmt.Fprint(writer, table)\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/auth/login.go",
    "content": "package auth\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/keyring\"\n\t\"github.com/buildkite/cli/v3/pkg/oauth\"\n\t\"github.com/google/uuid\"\n\t\"github.com/pkg/browser\"\n)\n\ntype LoginCmd struct {\n\tScopes string `help:\"OAuth scopes to request\" default:\"\"`\n\tOrg    string `help:\"Organization slug or UUID to request access for\" optional:\"\"`\n\tToken  string `help:\"API token to store (non-OAuth login, requires --org)\" optional:\"\"`\n}\n\nfunc organizationIdentifier(org string) (orgSlug, orgUUID string) {\n\tparsedUUID, err := uuid.Parse(org)\n\tif err == nil && strings.EqualFold(parsedUUID.String(), org) {\n\t\treturn \"\", org\n\t}\n\treturn org, \"\"\n}\n\nfunc (c *LoginCmd) Help() string {\n\treturn `\nAuthenticate with Buildkite using OAuth instead of manually creating an API token.\n\nBy default, the CLI requests all available scopes and Buildkite grants only those\nyour account has permission for. Use --scopes to request a specific subset instead.\n\nScope groups can be used as shorthand for common permission sets:\n  read_only    All read_* scopes (read-only access)\n\nGroups can be mixed with individual scopes:\n  --scopes \"read_only write_builds\"\n\nExamples:\n\n  # Login with full permissions (inherits your account's scopes)\n  $ bk auth login\n\n  # Login to a specific organization\n  $ bk auth login --org my-org\n\n  # Login non-interactively with an API token\n  $ bk auth login --org my-org --token my-token\n\n  # Login with read-only access\n  $ bk auth login --scopes read_only\n\n  # Login with read-only plus write access to builds\n  $ bk auth login --scopes \"read_only write_builds\"\n\n  # Login with specific scopes\n  $ bk auth login --scopes \"read_user read_organizations read_clusters write_clusters\"\n`\n}\n\n// LoginWithToken stores a token for an organization in the system keychain.\n// When the keychain is unavailable (e.g. BUILDKITE_NO_KEYRING=1 is set), it\n// still registers the org and selects it in config so that commands resolve the\n// org correctly; the caller is expected to supply the token via BUILDKITE_API_TOKEN.\nfunc LoginWithToken(f *factory.Factory, org, token string) error {\n\tif org == \"\" {\n\t\treturn errors.New(\"--org is required when --token is provided\")\n\t}\n\tif token == \"\" {\n\t\treturn errors.New(\"--token cannot be empty\")\n\t}\n\n\tkr := keyring.New()\n\tif kr.IsAvailable() {\n\t\tif err := kr.Set(org, token); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to store token in keychain: %w\", err)\n\t\t}\n\t\tfmt.Println(\"Token stored securely in system keychain.\")\n\t} else {\n\t\tfmt.Println(\"Keychain unavailable; token not stored. Use BUILDKITE_API_TOKEN to supply your token at runtime.\")\n\t}\n\n\tif err := f.Config.EnsureOrganization(org); err != nil {\n\t\treturn fmt.Errorf(\"failed to register organization in config: %w\", err)\n\t}\n\n\tif err := f.Config.SelectOrganization(org, f.GitRepository != nil); err != nil {\n\t\treturn fmt.Errorf(\"failed to select organization: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *LoginCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.Token != \"\" {\n\t\tif err := LoginWithToken(f, c.Org, c.Token); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfmt.Printf(\"\\nSuccessfully authenticated with organization %q\\n\", c.Org)\n\t\treturn nil\n\t}\n\n\t// Resolve scope groups (e.g., \"read_only\" → individual read_* scopes).\n\t// When --scopes is empty, no scope parameter is sent and the token\n\t// inherits the user's full Buildkite permissions.\n\tresolvedScopes := oauth.ResolveScopes(c.Scopes)\n\n\torgSlug, orgUUID := organizationIdentifier(c.Org)\n\n\t// Create OAuth flow\n\tcfg := &oauth.Config{\n\t\t// Host default handled via NewFlow, omitted to allow usage of BUILDKITE_HOST\n\t\tClientID: oauth.DefaultClientID,\n\t\tOrgSlug:  orgSlug,\n\t\tOrgUUID:  orgUUID,\n\t\tScopes:   resolvedScopes,\n\t}\n\n\tflow, err := oauth.NewFlow(cfg)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize OAuth flow: %w\", err)\n\t}\n\tdefer flow.Close()\n\n\t// Get authorization URL\n\tauthURL := flow.AuthorizationURL()\n\n\tfmt.Println(\"Opening browser for authentication...\")\n\tfmt.Printf(\"If the browser doesn't open, visit:\\n  %s\\n\\n\", authURL)\n\n\t// Open browser\n\tif err := browser.OpenURL(authURL); err != nil {\n\t\tfmt.Printf(\"Could not open browser automatically: %v\\n\", err)\n\t}\n\n\t// Wait for callback with timeout\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)\n\tdefer cancel()\n\n\tfmt.Println(\"Waiting for authentication...\")\n\n\tresult, err := flow.WaitForCallback(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"authentication failed: %w\", err)\n\t}\n\n\t// Exchange code for token\n\tfmt.Println(\"Exchanging authorization code for token...\")\n\n\ttokenResp, err := flow.ExchangeCode(ctx, result.Code)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"token exchange failed: %w\", err)\n\t}\n\n\t// Resolve org from the API using the new token\n\tclient, err := buildkite.NewOpts(\n\t\tbuildkite.WithTokenAuth(tokenResp.AccessToken),\n\t\tbuildkite.WithBaseURL(f.Config.RESTAPIEndpoint()),\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create API client: %w\", err)\n\t}\n\n\torgs, _, err := client.Organizations.List(ctx, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list organizations: %w\", err)\n\t}\n\tif len(orgs) == 0 {\n\t\treturn fmt.Errorf(\"no organizations found for this token\")\n\t}\n\n\torg := orgs[0]\n\n\tif err := LoginWithToken(f, org.Slug, tokenResp.AccessToken); err != nil {\n\t\treturn err\n\t}\n\n\t// Store refresh token if the server issued one\n\tif tokenResp.RefreshToken != \"\" {\n\t\tkr := keyring.New()\n\t\tif kr.IsAvailable() {\n\t\t\tif err := kr.SetRefreshToken(org.Slug, tokenResp.RefreshToken); err != nil {\n\t\t\t\tfmt.Fprintf(os.Stderr, \"Warning: failed to store refresh token: %v\\n\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tfmt.Printf(\"\\n✅ Successfully authenticated with organization %q\\n\", org.Slug)\n\tfmt.Printf(\"  Scopes: %s\\n\", tokenResp.Scope)\n\tif tokenResp.RefreshToken != \"\" {\n\t\tfmt.Printf(\"  Token expires in: %s (will refresh automatically)\\n\", formatDuration(tokenResp.ExpiresIn))\n\t}\n\n\treturn nil\n}\n\nfunc formatDuration(seconds int) string {\n\tif seconds <= 0 {\n\t\treturn \"unknown\"\n\t}\n\td := time.Duration(seconds) * time.Second\n\tif d >= time.Hour {\n\t\thours := int(d.Hours())\n\t\tif hours == 1 {\n\t\t\treturn \"1 hour\"\n\t\t}\n\t\treturn fmt.Sprintf(\"%d hours\", hours)\n\t}\n\treturn fmt.Sprintf(\"%d minutes\", int(d.Minutes()))\n}\n"
  },
  {
    "path": "cmd/auth/login_test.go",
    "content": "package auth\n\nimport \"testing\"\n\nfunc TestOrganizationIdentifier(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\torg      string\n\t\twantSlug string\n\t\twantUUID string\n\t}{\n\t\t{\n\t\t\tname:     \"slug\",\n\t\t\torg:      \"buildkite\",\n\t\t\twantSlug: \"buildkite\",\n\t\t\twantUUID: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"uuid\",\n\t\t\torg:      \"018f2f7e-7e99-7d77-b4d3-a95cb01805f4\",\n\t\t\twantSlug: \"\",\n\t\t\twantUUID: \"018f2f7e-7e99-7d77-b4d3-a95cb01805f4\",\n\t\t},\n\t\t{\n\t\t\tname:     \"uppercase uuid\",\n\t\t\torg:      \"018F2F7E-7E99-7D77-B4D3-A95CB01805F4\",\n\t\t\twantSlug: \"\",\n\t\t\twantUUID: \"018F2F7E-7E99-7D77-B4D3-A95CB01805F4\",\n\t\t},\n\t\t{\n\t\t\tname:     \"uuid-like slug without hyphens\",\n\t\t\torg:      \"018f2f7e7e997d77b4d3a95cb01805f4\",\n\t\t\twantSlug: \"018f2f7e7e997d77b4d3a95cb01805f4\",\n\t\t\twantUUID: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgotSlug, gotUUID := organizationIdentifier(tt.org)\n\t\t\tif gotSlug != tt.wantSlug || gotUUID != tt.wantUUID {\n\t\t\t\tt.Fatalf(\"organizationIdentifier(%q) = (%q, %q), want (%q, %q)\", tt.org, gotSlug, gotUUID, tt.wantSlug, tt.wantUUID)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/auth/logout.go",
    "content": "package auth\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/keyring\"\n)\n\ntype LogoutCmd struct {\n\tAll bool   `help:\"Log out of all organizations\" xor:\"target\"`\n\tOrg string `help:\"Organization slug (defaults to currently selected organization)\" optional:\"\" xor:\"target\"`\n}\n\nfunc (c *LogoutCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.All {\n\t\treturn c.logoutAll(f)\n\t}\n\n\treturn c.logoutOrg(f)\n}\n\nfunc (c *LogoutCmd) logoutAll(f *factory.Factory) error {\n\torgs := f.Config.ConfiguredOrganizations()\n\n\tkr := keyring.New()\n\tif kr.IsAvailable() {\n\t\tfor _, org := range orgs {\n\t\t\tif err := kr.Delete(org); err != nil {\n\t\t\t\tfmt.Printf(\"Warning: could not remove token from keychain for %q: %v\\n\", org, err)\n\t\t\t}\n\t\t\t_ = kr.DeleteRefreshToken(org)\n\t\t}\n\t}\n\n\tif err := f.Config.ClearAllOrganizations(); err != nil {\n\t\treturn fmt.Errorf(\"failed to clear organizations from config: %w\", err)\n\t}\n\n\tfmt.Printf(\"Logged out of all %d organizations\\n\", len(orgs))\n\treturn nil\n}\n\nfunc (c *LogoutCmd) logoutOrg(f *factory.Factory) error {\n\torg := c.Org\n\tif org == \"\" {\n\t\torg = f.Config.OrganizationSlug()\n\t}\n\n\tif org == \"\" {\n\t\treturn fmt.Errorf(\"no organization specified and none currently selected\")\n\t}\n\n\tkr := keyring.New()\n\tif kr.IsAvailable() {\n\t\tif err := kr.Delete(org); err != nil {\n\t\t\tfmt.Printf(\"Warning: could not remove token from keychain: %v\\n\", err)\n\t\t} else {\n\t\t\tfmt.Println(\"Token removed from system keychain.\")\n\t\t}\n\t\t_ = kr.DeleteRefreshToken(org)\n\t}\n\n\tfmt.Printf(\"Logged out of organization %q\\n\", org)\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/auth/status.go",
    "content": "// Package auth handles commands related to authentication via the CLI\npackage auth\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\t\"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype StatusOutput struct {\n\tOrganizationSlug string                `json:\"organization_slug\"`\n\tToken            buildkite.AccessToken `json:\"token\"`\n}\n\nfunc (w StatusOutput) TextOutput() string {\n\tb := strings.Builder{}\n\n\tfmt.Fprintf(&b, \"Current organization: %s\\n\", w.OrganizationSlug)\n\tb.WriteRune('\\n')\n\tfmt.Fprintf(&b, \"API Token UUID:        %s\\n\", w.Token.UUID)\n\tfmt.Fprintf(&b, \"API Token Description: %s\\n\", w.Token.Description)\n\tfmt.Fprintf(&b, \"API Token Scopes:      %v\\n\", w.Token.Scopes)\n\tb.WriteRune('\\n')\n\tfmt.Fprintf(&b, \"API Token user name:  %s\\n\", w.Token.User.Name)\n\tfmt.Fprintf(&b, \"API Token user email: %s\\n\", w.Token.User.Email)\n\n\treturn b.String()\n}\n\ntype StatusCmd struct {\n\toutput.OutputFlags\n}\n\nfunc (c *StatusCmd) Help() string {\n\treturn `\nIt returns information on the current session.\n\nExamples:\n\t# List the current token session\n\t$ bk auth status\n`\n}\n\nfunc (c *StatusCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif validationErr := validation.ValidateConfiguration(f.Config, kongCtx.Command()); validationErr != nil {\n\t\treturn validationErr\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\torgSlug := f.Config.OrganizationSlug()\n\n\tif orgSlug == \"\" {\n\t\torgSlug = \"<None>\"\n\t}\n\n\ttoken, _, err := f.RestAPIClient.AccessTokens.Get(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get access token: %w\", err)\n\t}\n\n\tw := StatusOutput{\n\t\tOrganizationSlug: orgSlug,\n\t\tToken:            token,\n\t}\n\n\terr = output.Write(os.Stdout, w, format)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write output: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/auth/switch.go",
    "content": "package auth\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\ntype SwitchCmd struct {\n\tOrganizationSlug string `arg:\"\" optional:\"\" help:\"Organization slug to switch\"`\n}\n\nfunc (c *SwitchCmd) Help() string {\n\treturn `Select a configured organization.\n\nExamples:\n\t# Switch the 'my-cool-org' configuration\n\t$ bk auth switch my-cool-org\n\n\t# Interactively select an organization\n\t$ bk auth switch\n`\n}\n\nfunc (c *SwitchCmd) Run(globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.NoInput = globals.DisableInput()\n\n\tvar org *string\n\tif c.OrganizationSlug != \"\" {\n\t\torg = &c.OrganizationSlug\n\t}\n\n\treturn switchRun(org, f.Config, f.GitRepository != nil, f.NoInput)\n}\n\nfunc switchRun(org *string, conf *config.Config, inGitRepo bool, noInput bool) error {\n\tvar selected string\n\n\t// prompt to choose from configured orgs if one is not already selected\n\tif org == nil {\n\t\tvar err error\n\t\tselected, err = io.PromptForOne(\"organization\", conf.ConfiguredOrganizations(), noInput)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tselected = *org\n\t}\n\n\t// if already selected, do nothing\n\tif conf.OrganizationSlug() == selected {\n\t\tfmt.Printf(\"Using configuration for `%s`\\n\", selected)\n\t\treturn nil\n\t}\n\n\t// if the selected org exists, switch it\n\tif conf.HasConfiguredOrganization(selected) {\n\t\tfmt.Printf(\"Using configuration for `%s`\\n\", selected)\n\t\treturn conf.SelectOrganization(selected, inGitRepo)\n\t}\n\n\t// if the selected org doesnt exist, recommend configuring it and error out\n\treturn fmt.Errorf(\"no configuration found for `%s`. run `bk auth login` to add it\", selected)\n}\n"
  },
  {
    "path": "cmd/auth/switch_test.go",
    "content": "package auth\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc setEnv(t *testing.T, key, value string) {\n\toriginal, had := os.LookupEnv(key)\n\tif err := os.Setenv(key, value); err != nil {\n\t\tt.Fatalf(\"failed to set env %s: %v\", key, err)\n\t}\n\tt.Cleanup(func() {\n\t\tvar restoreErr error\n\t\tif had {\n\t\t\trestoreErr = os.Setenv(key, original)\n\t\t} else {\n\t\t\trestoreErr = os.Unsetenv(key)\n\t\t}\n\t\tif restoreErr != nil {\n\t\t\tt.Fatalf(\"failed to restore env %s: %v\", key, restoreErr)\n\t\t}\n\t})\n}\n\nfunc TestCmdSwitch(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"switches already selected org\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\tconf.SelectOrganization(\"testing\", true)\n\t\tselected := \"testing\"\n\t\terr := switchRun(&selected, conf, true, false)\n\t\tif err != nil {\n\t\t\tt.Error(\"expected no error\")\n\t\t}\n\t\tif conf.OrganizationSlug() != \"testing\" {\n\t\t\tt.Error(\"expected no change in organization\")\n\t\t}\n\t})\n\n\tt.Run(\"switches existing org\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// add some configurations\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\t\tconf.SelectOrganization(\"testing\", true)\n\t\tconf.EnsureOrganization(\"testing\")\n\t\tconf.EnsureOrganization(\"default\")\n\t\t// now get a new empty config\n\t\tconf = config.New(fs, nil)\n\t\tselected := \"testing\"\n\t\terr := switchRun(&selected, conf, true, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"expected no error: %s\", err)\n\t\t}\n\t\tif conf.OrganizationSlug() != \"testing\" {\n\t\t\tt.Error(\"expected no change in organization\")\n\t\t}\n\t})\n\n\tt.Run(\"errors if missing org\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tselected := \"testing\"\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\terr := switchRun(&selected, conf, true, false)\n\t\tif err == nil {\n\t\t\tt.Error(\"expected an error\")\n\t\t}\n\t})\n\n\tt.Run(\"reads organization from user's config file\", func(t *testing.T) {\n\t\thome := t.TempDir()\n\t\tsetEnv(t, \"HOME\", home)\n\t\txdgConfig := filepath.Join(home, \".config\")\n\t\tsetEnv(t, \"XDG_CONFIG_HOME\", xdgConfig)\n\t\tsetEnv(t, \"BUILDKITE_API_TOKEN\", \"\")\n\t\tsetEnv(t, \"BUILDKITE_ORGANIZATION_SLUG\", \"\")\n\t\tif err := os.MkdirAll(xdgConfig, 0o755); err != nil {\n\t\t\tt.Fatalf(\"failed to create config dir: %v\", err)\n\t\t}\n\n\t\tswitchrConfigPath := filepath.Join(xdgConfig, \"bk.yaml\")\n\t\tcontent := []byte(\"selected_org: testing\\norganizations:\\n  testing:\\n    api_token: token-123\\n\")\n\t\tif err := os.WriteFile(switchrConfigPath, content, 0o644); err != nil {\n\t\t\tt.Fatalf(\"failed to write switchr config: %v\", err)\n\t\t}\n\n\t\tconf := config.New(afero.NewOsFs(), nil)\n\t\tif got := conf.OrganizationSlug(); got != \"testing\" {\n\t\t\tt.Fatalf(\"expected organization from file, got %q\", got)\n\t\t}\n\t\tif got := conf.APIToken(); got != \"token-123\" {\n\t\t\tt.Fatalf(\"expected token from file, got %q\", got)\n\t\t}\n\n\t\tselected := \"testing\"\n\t\tif err := switchRun(&selected, conf, false, true); err != nil {\n\t\t\tt.Fatalf(\"expected switchRun to succeed: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"preserves organization name case\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestCases := []struct {\n\t\t\tname    string\n\t\t\torgName string\n\t\t}{\n\t\t\t{\"mixed case\", \"gridX\"},\n\t\t\t{\"uppercase\", \"ACME\"},\n\t\t\t{\"lowercase\", \"buildkite\"},\n\t\t\t{\"camelCase\", \"myOrg\"},\n\t\t}\n\n\t\tfor _, tc := range testCases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\tfs := afero.NewMemMapFs()\n\t\t\t\tconf := config.New(fs, nil)\n\n\t\t\t\t// Configure organization with specific case\n\t\t\t\tif err := conf.EnsureOrganization(tc.orgName); err != nil {\n\t\t\t\t\tt.Fatalf(\"EnsureOrganization failed: %v\", err)\n\t\t\t\t}\n\t\t\t\tif err := conf.SelectOrganization(tc.orgName, false); err != nil {\n\t\t\t\t\tt.Fatalf(\"SelectOrganization failed: %v\", err)\n\t\t\t\t}\n\n\t\t\t\t// Switch the organization\n\t\t\t\tif err := switchRun(&tc.orgName, conf, false, true); err != nil {\n\t\t\t\t\tt.Fatalf(\"switchRun failed: %v\", err)\n\t\t\t\t}\n\n\t\t\t\t// Verify case is preserved\n\t\t\t\tgotOrg := conf.OrganizationSlug()\n\t\t\t\tif gotOrg != tc.orgName {\n\t\t\t\t\tt.Errorf(\"expected organization %q, got %q - case was not preserved\", tc.orgName, gotOrg)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "cmd/auth/token.go",
    "content": "package auth\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\ntype TokenCmd struct{}\n\nfunc (c *TokenCmd) Help() string {\n\treturn `\nPrints the stored API token for the currently selected organization to stdout.\n\nThe token is retrieved from the system keychain (or the BUILDKITE_API_TOKEN\nenvironment variable if set). This is useful for passing the token to other\ntools, for example:\n\nExamples:\n\t# Print the current token\n\t$ bk auth token\n\n\t# Use the token in a curl request\n\t$ curl -H \"Authorization: Bearer $(bk auth token)\" https://api.buildkite.com/v2/user\n`\n}\n\nfunc (c *TokenCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttoken := f.Config.APIToken()\n\tif token == \"\" {\n\t\treturn fmt.Errorf(\"no token found; run `bk auth login` to authenticate\")\n\t}\n\n\tfmt.Fprintln(os.Stdout, token)\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/build/cancel.go",
    "content": "package build\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/alecthomas/kong\"\n\tbuildResolver \"github.com/buildkite/cli/v3/internal/build/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/util\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype CancelCmd struct {\n\tBuildNumber string `arg:\"\" help:\"Build number to cancel\"`\n\tPipeline    string `help:\"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.\" short:\"p\"`\n\tWeb         bool   `help:\"Open the build in a web browser after it has been cancelled.\" short:\"w\"`\n}\n\nfunc (c *CancelCmd) Help() string {\n\treturn `\nExamples:\n  # Cancel a build by number\n  $ bk build cancel 123 --pipeline my-pipeline\n\n  # Cancel a build and open in browser\n  $ bk build cancel 123 -pipeline my-pipeline --web`\n}\n\nfunc (c *CancelCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\tpipelineRes := pipelineResolver.NewAggregateResolver(\n\t\tpipelineResolver.ResolveFromFlag(c.Pipeline, f.Config),\n\t\tpipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)),\n\t\tpipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOneWithFactory(f))),\n\t)\n\n\targs := []string{c.BuildNumber}\n\tbuildRes := buildResolver.NewAggregateResolver(\n\t\tbuildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config),\n\t)\n\n\tbld, err := buildRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconfirmed, err := bkIO.Confirm(f, fmt.Sprintf(\"Cancel build #%d on %s\", bld.BuildNumber, bld.Pipeline))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !confirmed {\n\t\treturn nil\n\t}\n\n\treturn cancelBuild(ctx, bld.Organization, bld.Pipeline, fmt.Sprint(bld.BuildNumber), c.Web, f)\n}\n\nfunc cancelBuild(ctx context.Context, org string, pipeline string, buildId string, web bool, f *factory.Factory) error {\n\tvar build buildkite.Build\n\tif err := bkIO.SpinWhile(f, fmt.Sprintf(\"Cancelling build #%s from pipeline %s\", buildId, pipeline), func() error {\n\t\tvar apiErr error\n\t\tbuild, apiErr = f.RestAPIClient.Builds.Cancel(ctx, org, pipeline, buildId)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Printf(\"%s\\n\", renderResult(fmt.Sprintf(\"Build canceled: %s\", build.WebURL)))\n\n\treturn util.OpenInWebBrowser(web, build.WebURL)\n}\n"
  },
  {
    "path": "cmd/build/create.go",
    "content": "package build\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkErrors \"github.com/buildkite/cli/v3/internal/errors\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/util\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype CreateCmd struct {\n\tMessage             string   `help:\"Description of the build. If left blank, the commit message will be used once the build starts.\" short:\"m\"`\n\tCommit              string   `help:\"The commit to build.\" short:\"c\" default:\"HEAD\"`\n\tBranch              string   `help:\"The branch to build. Defaults to the default branch of the pipeline.\" short:\"b\"`\n\tAuthor              string   `help:\"Author of the build. Supports: \\\"Name <email>\\\", \\\"email@domain.com\\\", \\\"Full Name\\\", or \\\"username\\\"\" short:\"a\"`\n\tWeb                 bool     `help:\"Open the build in a web browser after it has been created.\" short:\"w\"`\n\tPipeline            string   `help:\"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.\" short:\"p\"`\n\tEnv                 []string `help:\"Set environment variables for the build (KEY=VALUE)\" short:\"e\" sep:\"none\"`\n\tMetadata            []string `help:\"Set metadata for the build (KEY=VALUE)\" short:\"M\" sep:\"none\"`\n\tIgnoreBranchFilters bool     `help:\"Ignore branch filters for the pipeline\" short:\"i\"`\n\tEnvFile             string   `help:\"Set the environment variables for the build via an environment file\" short:\"f\"`\n}\n\nfunc (c *CreateCmd) Help() string {\n\treturn `The web URL to the build will be printed to stdout.\n\nExamples:\n  # Create a new build\n  $ bk build create\n\n  # Create a new build with environment variables set\n  $ bk build create -e \"FOO=BAR\" -e \"BAR=BAZ\"\n\n  # Create a new build with metadata\n  $ bk build create -M \"key=value\" -M \"foo=bar\"`\n}\n\nfunc (c *CreateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\t// Initialize factory\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn bkErrors.NewInternalError(err, \"failed to initialize CLI\", \"This is likely a bug\", \"Report to Buildkite\")\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\tresolvers := resolver.NewAggregateResolver(\n\t\tresolver.ResolveFromFlag(c.Pipeline, f.Config),\n\t\tresolver.ResolveFromConfig(f.Config, resolver.PickOneWithFactory(f)),\n\t\tresolver.ResolveFromRepository(f, resolver.CachedPicker(f.Config, resolver.PickOneWithFactory(f))),\n\t)\n\n\tresolvedPipeline, err := resolvers.Resolve(ctx)\n\tif err != nil {\n\t\treturn err // Already wrapped by resolver\n\t}\n\tif resolvedPipeline == nil {\n\t\treturn bkErrors.NewResourceNotFoundError(\n\t\t\tnil,\n\t\t\t\"could not resolve a pipeline\",\n\t\t\t\"Specify a pipeline with --pipeline (-p)\",\n\t\t\t\"Run 'bk pipeline list' to see available pipelines\",\n\t\t)\n\t}\n\n\tconfirmed, err := bkIO.Confirm(f, fmt.Sprintf(\"Create new build on %s?\", resolvedPipeline.Name))\n\tif err != nil {\n\t\treturn bkErrors.NewUserAbortedError(err, \"confirmation canceled\")\n\t}\n\n\tif !confirmed {\n\t\tfmt.Println(\"Build creation canceled\")\n\t\treturn nil\n\t}\n\n\t// Process environment variables\n\tenvMap := make(map[string]string)\n\tfor _, e := range c.Env {\n\t\tkey, value, _ := strings.Cut(e, \"=\")\n\t\tenvMap[key] = value\n\t}\n\n\t// Process metadata variables\n\tmetaDataMap := make(map[string]string)\n\tfor _, m := range c.Metadata {\n\t\tkey, value, _ := strings.Cut(m, \"=\")\n\t\tmetaDataMap[key] = value\n\t}\n\n\t// Process environment file if specified\n\tif c.EnvFile != \"\" {\n\t\tfile, err := os.Open(c.EnvFile)\n\t\tif err != nil {\n\t\t\treturn bkErrors.NewValidationError(\n\t\t\t\terr,\n\t\t\t\tfmt.Sprintf(\"could not open environment file: %s\", c.EnvFile),\n\t\t\t\t\"Check that the file exists and is readable\",\n\t\t\t)\n\t\t}\n\t\tdefer file.Close()\n\n\t\tcontent := bufio.NewScanner(file)\n\t\tfor content.Scan() {\n\t\t\tkey, value, _ := strings.Cut(content.Text(), \"=\")\n\t\t\tenvMap[key] = value\n\t\t}\n\n\t\tif err := content.Err(); err != nil {\n\t\t\treturn bkErrors.NewValidationError(\n\t\t\t\terr,\n\t\t\t\t\"error reading environment file\",\n\t\t\t\t\"Ensure the file contains valid environment variables in KEY=VALUE format\",\n\t\t\t)\n\t\t}\n\t}\n\n\treturn createBuild(ctx, resolvedPipeline.Org, resolvedPipeline.Name, f, c.Message, c.Commit, c.Branch, c.Web, envMap, metaDataMap, c.IgnoreBranchFilters, c.Author)\n}\n\nfunc parseAuthor(author string) buildkite.Author {\n\tif author == \"\" {\n\t\treturn buildkite.Author{}\n\t}\n\n\t// Check for Git-style format: \"Name <email@domain.com>\"\n\tif strings.Contains(author, \"<\") && strings.Contains(author, \">\") {\n\t\tparts := strings.Split(author, \"<\")\n\t\tif len(parts) == 2 {\n\t\t\tname := strings.TrimSpace(parts[0])\n\t\t\temail := strings.TrimSpace(strings.Trim(parts[1], \">\"))\n\t\t\tif name != \"\" && email != \"\" {\n\t\t\t\treturn buildkite.Author{Name: name, Email: email}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for email-only format\n\tif strings.Contains(author, \"@\") && strings.Contains(author, \".\") && !strings.Contains(author, \" \") {\n\t\treturn buildkite.Author{Email: author}\n\t}\n\n\t// Check for name format (contains spaces but no email)\n\tif strings.Contains(author, \" \") {\n\t\treturn buildkite.Author{Name: author}\n\t}\n\n\t// Default to username\n\treturn buildkite.Author{Username: author}\n}\n\nfunc createBuild(ctx context.Context, org string, pipeline string, f *factory.Factory, message string, commit string, branch string, web bool, env map[string]string, metaData map[string]string, ignoreBranchFilters bool, author string) error {\n\tvar build buildkite.Build\n\tif err := bkIO.SpinWhile(f, fmt.Sprintf(\"Starting new build for %s\", pipeline), func() error {\n\t\tbranch = strings.TrimSpace(branch)\n\t\tif len(branch) == 0 {\n\t\t\tp, _, err := f.RestAPIClient.Pipelines.Get(ctx, org, pipeline)\n\t\t\tif err != nil {\n\t\t\t\treturn bkErrors.WrapAPIError(err, \"fetching pipeline information\")\n\t\t\t}\n\n\t\t\t// Check if the pipeline has a default branch set\n\t\t\tif p.DefaultBranch == \"\" {\n\t\t\t\treturn bkErrors.NewValidationError(\n\t\t\t\t\tnil,\n\t\t\t\t\tfmt.Sprintf(\"No default branch set for pipeline %s\", pipeline),\n\t\t\t\t\t\"Please specify a branch using --branch (-b)\",\n\t\t\t\t\t\"Set a default branch in your pipeline settings on Buildkite\",\n\t\t\t\t)\n\t\t\t}\n\t\t\tbranch = p.DefaultBranch\n\t\t}\n\n\t\tnewBuild := buildkite.CreateBuild{\n\t\t\tMessage:                     message,\n\t\t\tCommit:                      commit,\n\t\t\tBranch:                      branch,\n\t\t\tAuthor:                      parseAuthor(author),\n\t\t\tEnv:                         env,\n\t\t\tMetaData:                    metaData,\n\t\t\tIgnorePipelineBranchFilters: ignoreBranchFilters,\n\t\t}\n\n\t\tvar err error\n\t\tbuild, _, err = f.RestAPIClient.Builds.Create(ctx, org, pipeline, newBuild)\n\t\tif err != nil {\n\t\t\treturn bkErrors.WrapAPIError(err, \"creating build\")\n\t\t}\n\t\treturn nil\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tif build.WebURL == \"\" {\n\t\treturn bkErrors.NewAPIError(\n\t\t\tnil,\n\t\t\t\"build was created but no URL was returned\",\n\t\t\t\"This may be due to an API version mismatch\",\n\t\t)\n\t}\n\n\tfmt.Printf(\"%s\\n\", renderResult(fmt.Sprintf(\"Build created: %s\", build.WebURL)))\n\n\tif err := util.OpenInWebBrowser(web, build.WebURL); err != nil {\n\t\treturn bkErrors.NewInternalError(err, \"failed to open web browser\")\n\t}\n\n\treturn nil\n}\n\nfunc renderResult(result string) string {\n\treturn result\n}\n"
  },
  {
    "path": "cmd/build/download.go",
    "content": "package build\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/build\"\n\tbuildResolver \"github.com/buildkite/cli/v3/internal/build/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/build/resolver/options\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n)\n\ntype DownloadCmd struct {\n\tBuildNumber string `arg:\"\" optional:\"\" help:\"Build number to download (omit for most recent build)\"`\n\tPipeline    string `help:\"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.\" short:\"p\"`\n\tBranch      string `help:\"Filter builds to this branch.\" short:\"b\"`\n\tUser        string `help:\"Filter builds to this user. You can use name or email.\" short:\"u\" xor:\"userfilter\"`\n\tMine        bool   `help:\"Filter builds to only my user.\" short:\"m\" xor:\"userfilter\"`\n}\n\nfunc (c *DownloadCmd) Help() string {\n\treturn `\nExamples:\n  # Download build 123\n  $ bk build download 123 --pipeline my-pipeline\n\n  # Download most recent build\n  $ bk build download --pipeline my-pipeline\n\n  # Download most recent build on a branch\n  $ bk build download -b main --pipeline my-pipeline\n\n  # Download most recent build by a user\n  $ bk build download --pipeline my-pipeline -u alice@hello.com\n\n  # Download most recent build by yourself\n  $ bk build download --pipeline my-pipeline --mine`\n}\n\nfunc (c *DownloadCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\t// we find the pipeline based on the following rules:\n\t// 1. an explicit flag is passed\n\t// 2. a configured pipeline for this directory\n\t// 3. find pipelines matching the current repository from the API\n\tpipelineRes := pipelineResolver.NewAggregateResolver(\n\t\tpipelineResolver.ResolveFromFlag(c.Pipeline, f.Config),\n\t\tpipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)),\n\t\tpipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOneWithFactory(f))),\n\t)\n\n\t// we resolve a build based on the following rules:\n\t// 1. an optional argument\n\t// 2. resolve from API using some context\n\t//    a. filter by branch if --branch or use current repo\n\t//    b. filter by user if --user or --mine given\n\toptionsResolver := options.AggregateResolver{\n\t\toptions.ResolveBranchFromFlag(c.Branch),\n\t\toptions.ResolveBranchFromRepository(f.GitRepository),\n\t}.WithResolverWhen(\n\t\tc.User != \"\",\n\t\toptions.ResolveUserFromFlag(c.User),\n\t).WithResolverWhen(\n\t\tc.Mine || c.User == \"\",\n\t\toptions.ResolveCurrentUser(ctx, f),\n\t)\n\n\targs := []string{}\n\tif c.BuildNumber != \"\" {\n\t\targs = []string{c.BuildNumber}\n\t}\n\tbuildRes := buildResolver.NewAggregateResolver(\n\t\tbuildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config),\n\t\tbuildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...),\n\t)\n\n\tbld, err := buildRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif bld == nil {\n\t\tfmt.Println(\"No build found.\")\n\t\treturn nil\n\t}\n\n\tvar dir string\n\tif err = bkIO.SpinWhile(f, \"Downloading build resources\", func() error {\n\t\tdir, err = download(ctx, bld, f)\n\t\treturn err\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Printf(\"Downloaded build to: %s\\n\", dir)\n\n\treturn nil\n}\n\nfunc download(ctx context.Context, build *build.Build, f *factory.Factory) (string, error) {\n\tvar wg sync.WaitGroup\n\tvar mu sync.Mutex\n\tb, _, err := f.RestAPIClient.Builds.Get(ctx, build.Organization, build.Pipeline, fmt.Sprint(build.BuildNumber), nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdirectory := fmt.Sprintf(\"build-%s\", b.ID)\n\terr = os.MkdirAll(directory, os.ModePerm)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, job := range b.Jobs {\n\t\t// only script (command) jobs will have logs\n\t\tif job.Type != \"script\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\twg.Add(1)\n\t\t\tlog, _, apiErr := f.RestAPIClient.Jobs.GetJobLog(ctx, build.Organization, build.Pipeline, b.ID, job.ID)\n\t\t\tif err != nil {\n\t\t\t\tmu.Lock()\n\t\t\t\terr = apiErr\n\t\t\t\tmu.Unlock()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfileErr := os.WriteFile(filepath.Join(directory, job.ID), []byte(log.Content), 0o644)\n\t\t\tif fileErr != nil {\n\t\t\t\tmu.Lock()\n\t\t\t\terr = fileErr\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t}()\n\t}\n\n\tartifacts, _, err := f.RestAPIClient.Artifacts.ListByBuild(ctx, build.Organization, build.Pipeline, fmt.Sprint(build.BuildNumber), nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, artifact := range artifacts {\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\twg.Add(1)\n\t\t\tout, fileErr := os.Create(filepath.Join(directory, fmt.Sprintf(\"artifact-%s-%s\", artifact.ID, artifact.Filename)))\n\t\t\tif err != nil {\n\t\t\t\terr = fileErr\n\t\t\t}\n\t\t\t_, apiErr := f.RestAPIClient.Artifacts.DownloadArtifactByURL(ctx, artifact.DownloadURL, out)\n\t\t\tif err != nil {\n\t\t\t\terr = apiErr\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn directory, nil\n}\n"
  },
  {
    "path": "cmd/build/list.go",
    "content": "package build\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/mail\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/internal/graphql\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nconst (\n\tmaxBuildLimit = 5000\n\tpageSize      = 100\n)\n\ntype ListCmd struct {\n\tPipeline string            `help:\"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.\" short:\"p\"`\n\tSince    string            `help:\"Filter builds created since this time (e.g. 1h, 30m)\"`\n\tUntil    string            `help:\"Filter builds created before this time (e.g. 1h, 30m)\"`\n\tDuration string            `help:\"Filter by duration (e.g. >5m, <10m, 20m) - supports >, <, >=, <= operators\"`\n\tState    []string          `help:\"Filter by build state\"`\n\tBranch   []string          `help:\"Filter by branch name\"`\n\tCreator  string            `help:\"Filter by creator (email address or user ID)\"`\n\tCommit   string            `help:\"Filter by commit SHA\"`\n\tMessage  string            `help:\"Filter by message content\"`\n\tMetaData map[string]string `help:\"Filter by build meta-data (key=value format, can be specified multiple times)\"`\n\tLimit    int               `help:\"Maximum number of builds to return\" default:\"50\"`\n\tNoLimit  bool              `help:\"Fetch all builds (overrides --limit)\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ListCmd) Help() string {\n\treturn `List builds with optional filtering.\n\nThis command supports both server-side filtering (fast) and client-side filtering.\nServer-side filters are applied by the Buildkite API, while client-side filters\nare applied after fetching results and may require loading more builds.\n\nClient-side filters: --duration, --message\nServer-side filters: --pipeline, --since, --until, --state, --branch, --creator, --commit, --meta-data\n\nBuilds can be filtered by their duration, message content, and other attributes.\nWhen filtering by duration, you can use operators like >, <, >=, and <= to specify your criteria.\nSupported duration units are seconds (s), minutes (m), and hours (h).\n\nExamples:\n  # List recent builds (50 by default)\n  $ bk build list\n\n  # Get more builds (automatically paginates)\n  $ bk build list --limit 500\n\n  # List builds from the last hour\n  $ bk build list --since 1h\n\n  # List failed builds\n  $ bk build list --state failed\n\n  # List builds on main branch\n  $ bk build list --branch main\n\n  # List builds by alice\n  $ bk build list --creator alice@company.com\n\n  # List builds that took longer than 20 minutes\n  $ bk build list --duration \">20m\"\n\n  # List builds that finished in under 5 minutes\n  $ bk build list --duration \"<5m\"\n\n  # Combine filters: failed builds on main branch in the last 24 hours\n  $ bk build list --state failed --branch main --since 24h\n\n  # Find builds containing \"deploy\" in the message\n  $ bk build list --message deploy\n\n  # Filter builds by meta-data\n  $ bk build list --meta-data env=production\n\n  # Filter by multiple meta-data keys\n  $ bk build list --meta-data env=production --meta-data deploy=true\n\n  # Complex filtering: slow builds (>30m) that failed on feature branches\n  $ bk build list --duration \">30m\" --state failed --branch feature/`\n}\n\nfunc (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\tif !c.NoLimit {\n\t\tif c.Limit > maxBuildLimit {\n\t\t\treturn fmt.Errorf(\"limit cannot exceed %d builds (requested: %d); if you need more, use --no-limit\", maxBuildLimit, c.Limit)\n\t\t}\n\t}\n\n\tif c.Creator != \"\" && isValidEmail(c.Creator) {\n\t\toriginalEmail := c.Creator\n\t\tif err = bkIO.SpinWhile(f, \"Looking up user\", func() error {\n\t\t\tc.Creator, err = resolveCreatorEmailToUserID(ctx, f, originalEmail)\n\t\t\treturn err\n\t\t}); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to resolve creator email: %w\", err)\n\t\t}\n\t\tif c.Creator == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to resolve creator email: no user found\")\n\t\t}\n\t}\n\n\tlistOpts, err := c.buildListOptions()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\torg := f.Config.OrganizationSlug()\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tif format == output.FormatText {\n\t\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\t\tdefer func() { _ = cleanup() }()\n\n\t\ttarget := org\n\t\tif c.Pipeline != \"\" {\n\t\t\ttarget = fmt.Sprintf(\"%s/%s\", org, c.Pipeline)\n\t\t}\n\n\t\tfmt.Fprintf(writer, \"Showing builds for %s\\n\\n\", target)\n\n\t\tbuilds, err := c.fetchBuilds(ctx, f, org, listOpts, format, writer)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to list builds: %w\", err)\n\t\t}\n\n\t\tif len(builds) == 0 {\n\t\t\tfmt.Fprintln(writer, \"No builds found matching the specified criteria.\")\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tbuilds, err := c.fetchBuilds(ctx, f, org, listOpts, format, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list builds: %w\", err)\n\t}\n\n\tif len(builds) == 0 {\n\t\treturn output.Write(os.Stdout, []buildkite.Build{}, format)\n\t}\n\n\treturn displayBuilds(builds, format, os.Stdout)\n}\n\nfunc (c *ListCmd) buildListOptions() (*buildkite.BuildsListOptions, error) {\n\tlistOpts := &buildkite.BuildsListOptions{\n\t\tListOptions: buildkite.ListOptions{\n\t\t\tPerPage: pageSize,\n\t\t},\n\t}\n\n\tnow := time.Now()\n\tif c.Since != \"\" {\n\t\td, err := time.ParseDuration(c.Since)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid since duration '%s': %w\", c.Since, err)\n\t\t}\n\t\tlistOpts.CreatedFrom = now.Add(-d)\n\t}\n\n\tif c.Until != \"\" {\n\t\td, err := time.ParseDuration(c.Until)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid until duration '%s': %w\", c.Until, err)\n\t\t}\n\t\tlistOpts.CreatedTo = now.Add(-d)\n\t}\n\n\tif len(c.State) > 0 {\n\t\tlistOpts.State = make([]string, len(c.State))\n\t\tfor i, state := range c.State {\n\t\t\tlistOpts.State[i] = strings.ToLower(state)\n\t\t}\n\t}\n\n\tlistOpts.Branch = c.Branch\n\tlistOpts.Creator = c.Creator\n\tlistOpts.Commit = c.Commit\n\n\tif len(c.MetaData) > 0 {\n\t\tlistOpts.MetaData = buildkite.MetaDataFilters{\n\t\t\tMetaData: c.MetaData,\n\t\t}\n\t}\n\n\treturn listOpts, nil\n}\n\nfunc (c *ListCmd) fetchBuilds(ctx context.Context, f *factory.Factory, org string, listOpts *buildkite.BuildsListOptions, format output.Format, writer io.Writer) ([]buildkite.Build, error) {\n\tvar allBuilds []buildkite.Build\n\n\t// filtered builds added since last confirm (used when --no-limit)\n\tfilteredSinceConfirm := 0\n\n\t// raw (unfiltered) build counters so progress messaging makes sense when client-side filters are active\n\trawTotalFetched := 0\n\trawSinceConfirm := 0\n\tpreviousPageFirstBuildNumber := 0\n\n\tprintedAny := false\n\n\tfor page := 1; ; page++ {\n\t\tif !c.NoLimit && len(allBuilds) >= c.Limit {\n\t\t\tbreak\n\t\t}\n\n\t\tlistOpts.Page = page\n\n\t\tvar builds []buildkite.Build\n\t\tvar err error\n\n\t\tspinnerMsg := \"Loading builds (\"\n\t\tif c.Pipeline != \"\" {\n\t\t\tspinnerMsg += fmt.Sprintf(\"pipeline %s, \", c.Pipeline)\n\t\t}\n\t\tfiltersActive := c.Duration != \"\" || c.Message != \"\"\n\n\t\t// Show matching (filtered) counts and raw counts independently\n\t\tif !c.NoLimit && c.Limit > 0 {\n\t\t\tspinnerMsg += fmt.Sprintf(\"%d/%d matching, %d raw fetched\", len(allBuilds), c.Limit, rawTotalFetched)\n\t\t} else {\n\t\t\tspinnerMsg += fmt.Sprintf(\"%d matching, %d raw fetched\", len(allBuilds), rawTotalFetched)\n\t\t}\n\t\tspinnerMsg += \")\"\n\n\t\tif format == output.FormatText && rawSinceConfirm >= maxBuildLimit {\n\t\t\tprompt := fmt.Sprintf(\"Fetched %d more builds (%d total). Continue?\", rawSinceConfirm, rawTotalFetched)\n\t\t\tif filtersActive {\n\t\t\t\tprompt = fmt.Sprintf(\n\t\t\t\t\t\"Fetched %d raw builds (%d matching, %d matching total). Continue?\",\n\t\t\t\t\trawSinceConfirm, filteredSinceConfirm, len(allBuilds),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tconfirmed, err := bkIO.Confirm(f, prompt)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif !confirmed {\n\t\t\t\treturn allBuilds, nil\n\t\t\t}\n\n\t\t\tfilteredSinceConfirm = 0\n\t\t\trawSinceConfirm = 0\n\t\t}\n\n\t\tif err = bkIO.SpinWhile(f, spinnerMsg, func() error {\n\t\t\tif c.Pipeline != \"\" {\n\t\t\t\tbuilds, err = c.getBuildsByPipeline(ctx, f, org, listOpts)\n\t\t\t} else {\n\t\t\t\tbuilds, _, err = f.RestAPIClient.Builds.ListByOrg(ctx, org, listOpts)\n\t\t\t}\n\t\t\treturn err\n\t\t}); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif len(builds) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// Track raw builds fetched before applying client-side filters\n\t\trawCountThisPage := len(builds)\n\t\trawTotalFetched += rawCountThisPage\n\t\trawSinceConfirm += rawCountThisPage\n\n\t\t// Detect duplicate first build number between pages to prevent infinite loop\n\t\tif page > 1 && len(builds) > 0 {\n\t\t\tcurrentPageFirstBuildNumber := builds[0].Number\n\t\t\tif currentPageFirstBuildNumber == previousPageFirstBuildNumber {\n\t\t\t\treturn nil, fmt.Errorf(\"API returned duplicate results, stopping to prevent infinite loop\")\n\t\t\t}\n\t\t}\n\n\t\tif len(builds) > 0 {\n\t\t\tpreviousPageFirstBuildNumber = builds[0].Number\n\t\t}\n\n\t\tbuilds, err = c.applyClientSideFilters(builds)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to apply filters: %w\", err)\n\t\t}\n\n\t\t// Decide which builds will actually be added (respect limit)\n\t\tvar buildsToAdd []buildkite.Build\n\t\taddedThisPage := 0\n\t\tif !c.NoLimit {\n\t\t\tremaining := c.Limit - len(allBuilds)\n\t\t\tif remaining <= 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif len(builds) > remaining {\n\t\t\t\tbuildsToAdd = builds[:remaining]\n\t\t\t\taddedThisPage = remaining\n\t\t\t} else {\n\t\t\t\tbuildsToAdd = builds\n\t\t\t\taddedThisPage = len(builds)\n\t\t\t}\n\t\t} else {\n\t\t\tbuildsToAdd = builds\n\t\t\taddedThisPage = len(builds)\n\t\t}\n\n\t\t// Stream only the builds we are about to add; header only once we actually print something\n\t\tif format == output.FormatText && len(buildsToAdd) > 0 && writer != nil {\n\t\t\t_ = displayBuilds(buildsToAdd, format, writer)\n\t\t\tif !printedAny {\n\t\t\t\tfmt.Fprintln(writer)\n\t\t\t}\n\t\t\tprintedAny = true\n\t\t}\n\n\t\tallBuilds = append(allBuilds, buildsToAdd...)\n\t\tfilteredSinceConfirm += addedThisPage\n\n\t\tif rawCountThisPage < listOpts.PerPage {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn allBuilds, nil\n}\n\nfunc (c *ListCmd) getBuildsByPipeline(ctx context.Context, f *factory.Factory, org string, listOpts *buildkite.BuildsListOptions) ([]buildkite.Build, error) {\n\tpipelineRes := pipelineResolver.NewAggregateResolver(\n\t\tpipelineResolver.ResolveFromFlag(c.Pipeline, f.Config),\n\t\tpipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)),\n\t)\n\n\tpipeline, err := pipelineRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbuilds, _, err := f.RestAPIClient.Builds.ListByPipeline(ctx, org, pipeline.Name, listOpts)\n\treturn builds, err\n}\n\nfunc (c *ListCmd) applyClientSideFilters(builds []buildkite.Build) ([]buildkite.Build, error) {\n\tif c.Duration == \"\" && c.Message == \"\" {\n\t\treturn builds, nil\n\t}\n\n\tvar durationOp string\n\tvar durationThreshold time.Duration\n\n\tif c.Duration != \"\" {\n\t\tdurationOp = \">=\"\n\t\tdurationStr := c.Duration\n\n\t\tswitch {\n\t\tcase strings.HasPrefix(c.Duration, \"<\"):\n\t\t\tdurationOp = \"<\"\n\t\t\tdurationStr = c.Duration[1:]\n\t\tcase strings.HasPrefix(c.Duration, \">\"):\n\t\t\tdurationOp = \">\"\n\t\t\tdurationStr = c.Duration[1:]\n\t\t}\n\n\t\td, err := time.ParseDuration(durationStr)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid duration format: %w\", err)\n\t\t}\n\t\tdurationThreshold = d\n\t}\n\n\tvar messageFilter string\n\tif c.Message != \"\" {\n\t\tmessageFilter = strings.ToLower(c.Message)\n\t}\n\n\tvar result []buildkite.Build\n\tfor _, build := range builds {\n\t\tif c.Duration != \"\" {\n\t\t\tif build.StartedAt == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar elapsed time.Duration\n\t\t\tif build.FinishedAt != nil {\n\t\t\t\telapsed = build.FinishedAt.Sub(build.StartedAt.Time)\n\t\t\t} else {\n\t\t\t\telapsed = time.Since(build.StartedAt.Time)\n\t\t\t}\n\n\t\t\tswitch durationOp {\n\t\t\tcase \"<\":\n\t\t\t\tif elapsed >= durationThreshold {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase \">\":\n\t\t\t\tif elapsed <= durationThreshold {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tif elapsed < durationThreshold {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif messageFilter != \"\" {\n\t\t\tif !strings.Contains(strings.ToLower(build.Message), messageFilter) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tresult = append(result, build)\n\t}\n\n\treturn result, nil\n}\n\nfunc isValidEmail(s string) bool {\n\t_, err := mail.ParseAddress(s)\n\treturn err == nil\n}\n\nfunc resolveCreatorEmailToUserID(ctx context.Context, f *factory.Factory, email string) (string, error) {\n\torg := f.Config.OrganizationSlug()\n\tresp, err := graphql.FindUserByEmail(ctx, f.GraphQLClient, org, email)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to query user by email: %w\", err)\n\t}\n\n\tif resp.Organization == nil || resp.Organization.Members == nil || len(resp.Organization.Members.Edges) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no user found with email: %s\", email)\n\t}\n\n\tmember := resp.Organization.Members.Edges[0].Node\n\tif member == nil {\n\t\treturn \"\", fmt.Errorf(\"invalid user data for email: %s\", email)\n\t}\n\n\t// Decode GraphQL ID and extract UUID\n\tdecoded, err := base64.StdEncoding.DecodeString(member.User.Id)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decode user ID: %w\", err)\n\t}\n\n\tif userUUID, found := strings.CutPrefix(string(decoded), \"User---\"); found {\n\t\treturn userUUID, nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"unexpected user ID format\")\n}\n\nfunc displayBuilds(builds []buildkite.Build, format output.Format, writer io.Writer) error {\n\tif format != output.FormatText {\n\t\treturn output.Write(writer, builds, format)\n\t}\n\n\tconst (\n\t\tmaxMessageLength = 22\n\t\ttruncatedLength  = 19\n\t\ttimeFormat       = \"2006-01-02T15:04:05Z\"\n\t)\n\n\tvar rows [][]string\n\n\tfor _, build := range builds {\n\t\tmessage := build.Message\n\t\tif len(message) > maxMessageLength {\n\t\t\tmessage = message[:truncatedLength] + \"...\"\n\t\t}\n\n\t\tstartedAt := \"-\"\n\t\tif build.StartedAt != nil {\n\t\t\tstartedAt = build.StartedAt.Format(timeFormat)\n\t\t}\n\n\t\tfinishedAt := \"-\"\n\t\tduration := \"-\"\n\t\tif build.FinishedAt != nil {\n\t\t\tfinishedAt = build.FinishedAt.Format(timeFormat)\n\t\t\tif build.StartedAt != nil {\n\t\t\t\tdur := build.FinishedAt.Sub(build.StartedAt.Time)\n\t\t\t\tduration = formatDuration(dur)\n\t\t\t}\n\t\t} else if build.StartedAt != nil {\n\t\t\tdur := time.Since(build.StartedAt.Time)\n\t\t\tduration = formatDuration(dur) + \" (running)\"\n\t\t}\n\n\t\trows = append(rows, []string{\n\t\t\tfmt.Sprintf(\"%d\", build.Number),\n\t\t\tbuild.State,\n\t\t\tmessage,\n\t\t\tstartedAt,\n\t\t\tfinishedAt,\n\t\t\tduration,\n\t\t\tbuild.WebURL,\n\t\t})\n\t}\n\n\theaders := []string{\"Number\", \"State\", \"Message\", \"Started (UTC)\", \"Finished (UTC)\", \"Duration\", \"URL\"}\n\ttable := output.Table(headers, rows, map[string]string{\n\t\t\"number\":         \"bold\",\n\t\t\"state\":          \"bold\",\n\t\t\"message\":        \"italic\",\n\t\t\"started (utc)\":  \"dim\",\n\t\t\"finished (utc)\": \"dim\",\n\t\t\"duration\":       \"bold\",\n\t\t\"url\":            \"dim\",\n\t})\n\tfmt.Fprint(writer, table)\n\treturn nil\n}\n\nfunc formatDuration(d time.Duration) string {\n\tif d < time.Minute {\n\t\treturn fmt.Sprintf(\"%.0fs\", d.Seconds())\n\t}\n\tif d < time.Hour {\n\t\tminutes := d / time.Minute\n\t\tseconds := (d % time.Minute) / time.Second\n\t\treturn fmt.Sprintf(\"%dm%ds\", minutes, seconds)\n\t}\n\thours := d / time.Hour\n\tminutes := (d % time.Hour) / time.Minute\n\treturn fmt.Sprintf(\"%dh%dm\", hours, minutes)\n}\n"
  },
  {
    "path": "cmd/build/list_test.go",
    "content": "package build\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype buildListOptions struct {\n\tduration string\n\tmessage  string\n}\n\nfunc applyClientSideFilters(builds []buildkite.Build, opts buildListOptions) ([]buildkite.Build, error) {\n\tcmd := &ListCmd{\n\t\tDuration: opts.duration,\n\t\tMessage:  opts.message,\n\t}\n\treturn cmd.applyClientSideFilters(builds)\n}\n\nfunc TestBuildListOptions_MetaData(t *testing.T) {\n\tcmd := &ListCmd{\n\t\tMetaData: map[string]string{\n\t\t\t\"env\":    \"production\",\n\t\t\t\"deploy\": \"true\",\n\t\t},\n\t}\n\n\topts, err := cmd.buildListOptions()\n\tif err != nil {\n\t\tt.Fatalf(\"buildListOptions failed: %v\", err)\n\t}\n\n\tif len(opts.MetaData.MetaData) != 2 {\n\t\tt.Errorf(\"Expected 2 meta-data filters, got %d\", len(opts.MetaData.MetaData))\n\t}\n\n\tif opts.MetaData.MetaData[\"env\"] != \"production\" {\n\t\tt.Errorf(\"Expected env=production, got env=%s\", opts.MetaData.MetaData[\"env\"])\n\t}\n\n\tif opts.MetaData.MetaData[\"deploy\"] != \"true\" {\n\t\tt.Errorf(\"Expected deploy=true, got deploy=%s\", opts.MetaData.MetaData[\"deploy\"])\n\t}\n}\n\nfunc TestBuildListOptions_EmptyMetaData(t *testing.T) {\n\tcmd := &ListCmd{}\n\n\topts, err := cmd.buildListOptions()\n\tif err != nil {\n\t\tt.Fatalf(\"buildListOptions failed: %v\", err)\n\t}\n\n\tif len(opts.MetaData.MetaData) != 0 {\n\t\tt.Errorf(\"Expected empty meta-data, got %d entries\", len(opts.MetaData.MetaData))\n\t}\n}\n\nfunc TestDisplayBuilds_EmptyJSON(t *testing.T) {\n\tvar buf bytes.Buffer\n\terr := displayBuilds([]buildkite.Build{}, output.FormatJSON, &buf)\n\tif err != nil {\n\t\tt.Fatalf(\"displayBuilds failed: %v\", err)\n\t}\n\n\tgot := strings.TrimSpace(buf.String())\n\tif got != \"[]\" {\n\t\tt.Errorf(\"Expected empty JSON array '[]', got %q\", got)\n\t}\n}\n\nfunc TestDisplayBuilds_EmptyYAML(t *testing.T) {\n\tvar buf bytes.Buffer\n\terr := displayBuilds([]buildkite.Build{}, output.FormatYAML, &buf)\n\tif err != nil {\n\t\tt.Fatalf(\"displayBuilds failed: %v\", err)\n\t}\n\n\tgot := strings.TrimSpace(buf.String())\n\tif got != \"[]\" {\n\t\tt.Errorf(\"Expected empty YAML array '[]', got %q\", got)\n\t}\n}\n\nfunc TestFilterBuilds(t *testing.T) {\n\tnow := time.Now()\n\tbuilds := []buildkite.Build{\n\t\t{\n\t\t\tNumber:     1,\n\t\t\tMessage:    \"Fast build\",\n\t\t\tStartedAt:  &buildkite.Timestamp{Time: now.Add(-5 * time.Minute)},\n\t\t\tFinishedAt: &buildkite.Timestamp{Time: now.Add(-4 * time.Minute)}, // 1 minute\n\t\t},\n\t\t{\n\t\t\tNumber:     2,\n\t\t\tMessage:    \"Long build\",\n\t\t\tStartedAt:  &buildkite.Timestamp{Time: now.Add(-30 * time.Minute)},\n\t\t\tFinishedAt: &buildkite.Timestamp{Time: now.Add(-10 * time.Minute)}, // 20 minutes\n\t\t},\n\t}\n\n\topts := buildListOptions{duration: \"10m\"}\n\tfiltered, err := applyClientSideFilters(builds, opts)\n\tif err != nil {\n\t\tt.Fatalf(\"applyClientSideFilters failed: %v\", err)\n\t}\n\n\tif len(filtered) != 1 {\n\t\tt.Errorf(\"Expected 1 build >= 10m, got %d\", len(filtered))\n\t}\n\n\topts = buildListOptions{message: \"Fast\"}\n\tfiltered, err = applyClientSideFilters(builds, opts)\n\tif err != nil {\n\t\tt.Fatalf(\"applyClientSideFilters failed: %v\", err)\n\t}\n\n\tif len(filtered) != 1 {\n\t\tt.Errorf(\"Expected 1 build with 'Fast', got %d\", len(filtered))\n\t}\n}\n"
  },
  {
    "path": "cmd/build/rebuild.go",
    "content": "package build\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/alecthomas/kong\"\n\tbuildResolver \"github.com/buildkite/cli/v3/internal/build/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/build/resolver/options\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/util\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype RebuildCmd struct {\n\tBuildNumber string `arg:\"\" optional:\"\" help:\"Build number to rebuild (omit for most recent build)\"`\n\tPipeline    string `help:\"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.\" short:\"p\"`\n\tBranch      string `help:\"Filter builds to this branch.\" short:\"b\"`\n\tUser        string `help:\"Filter builds to this user. You can use name or email.\" short:\"u\" xor:\"userfilter\"`\n\tMine        bool   `help:\"Filter builds to only my user.\" short:\"m\" xor:\"userfilter\"`\n\tWeb         bool   `help:\"Open the build in a web browser after it has been created.\" short:\"w\"`\n}\n\nfunc (c *RebuildCmd) Help() string {\n\treturn `\nExamples:\n  # Rebuild a specific build by number\n  $ bk build rebuild 123\n\n  # Rebuild most recent build\n  $ bk build rebuild\n\n  # Rebuild and open in browser\n  $ bk build rebuild 123 --web\n\n  # Rebuild most recent build on a branch\n  $ bk build rebuild -b main\n\n  # Rebuild most recent build by a user\n  $ bk build rebuild -u alice\n\n  # Rebuild most recent build by yourself\n  $ bk build rebuild --mine`\n}\n\nfunc (c *RebuildCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\t// we find the pipeline based on the following rules:\n\t// 1. an explicit flag is passed\n\t// 2. a configured pipeline for this directory\n\t// 3. find pipelines matching the current repository from the API\n\tpipelineRes := pipelineResolver.NewAggregateResolver(\n\t\tpipelineResolver.ResolveFromFlag(c.Pipeline, f.Config),\n\t\tpipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)),\n\t\tpipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOneWithFactory(f))),\n\t)\n\n\t// we resolve a build based on the following rules:\n\t// 1. an optional argument\n\t// 2. resolve from API using some context\n\t//    a. filter by branch if --branch or use current repo\n\t//    b. filter by user if --user or --mine given\n\toptionsResolver := options.AggregateResolver{\n\t\toptions.ResolveBranchFromFlag(c.Branch),\n\t\toptions.ResolveBranchFromRepository(f.GitRepository),\n\t}.WithResolverWhen(\n\t\tc.User != \"\",\n\t\toptions.ResolveUserFromFlag(c.User),\n\t).WithResolverWhen(\n\t\tc.Mine || c.User == \"\",\n\t\toptions.ResolveCurrentUser(ctx, f),\n\t)\n\n\targs := []string{}\n\tif c.BuildNumber != \"\" {\n\t\targs = []string{c.BuildNumber}\n\t}\n\tbuildRes := buildResolver.NewAggregateResolver(\n\t\tbuildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config),\n\t\tbuildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...),\n\t)\n\n\tbld, err := buildRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif bld == nil {\n\t\tfmt.Println(\"No build found.\")\n\t\treturn nil\n\t}\n\n\treturn rebuild(ctx, bld.Organization, bld.Pipeline, fmt.Sprint(bld.BuildNumber), c.Web, f)\n}\n\nfunc rebuild(ctx context.Context, org string, pipeline string, buildId string, web bool, f *factory.Factory) error {\n\tvar build buildkite.Build\n\tif err := bkIO.SpinWhile(f, fmt.Sprintf(\"Rerunning build #%s for pipeline %s\", buildId, pipeline), func() error {\n\t\tvar apiErr error\n\t\tbuild, apiErr = f.RestAPIClient.Builds.Rebuild(ctx, org, pipeline, buildId)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Printf(\"%s\\n\", renderResult(fmt.Sprintf(\"Build created: %s\", build.WebURL)))\n\n\treturn util.OpenInWebBrowser(web, build.WebURL)\n}\n"
  },
  {
    "path": "cmd/build/view.go",
    "content": "package build\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/alecthomas/kong\"\n\tbuildResolver \"github.com/buildkite/cli/v3/internal/build/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/build/resolver/options\"\n\t\"github.com/buildkite/cli/v3/internal/build/view\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\t\"github.com/pkg/browser\"\n)\n\ntype ViewCmd struct {\n\tBuildNumber string   `arg:\"\" optional:\"\" help:\"Build number to view (omit for most recent build)\"`\n\tPipeline    string   `help:\"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.\" short:\"p\"`\n\tBranch      string   `help:\"Filter builds to this branch.\" short:\"b\"`\n\tUser        string   `help:\"Filter builds to this user. You can use name or email.\" short:\"u\" xor:\"userfilter\"`\n\tMine        bool     `help:\"Filter builds to only my user.\" xor:\"userfilter\"`\n\tJobStates   []string `help:\"Filter jobs by state. Valid states: running, scheduled, passed, failed, canceled, skipped, not_run, broken.\" short:\"s\" sep:\",\"`\n\tWeb         bool     `help:\"Open the build in a web browser.\" short:\"w\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ViewCmd) Help() string {\n\treturn `You can pass an optional build number to view. If omitted, the most recent build on the current branch will be resolved.\n\nExamples:\n  # By default, the most recent build for the current branch is shown\n  $ bk build view\n\n  # If not inside a repository or to use a specific pipeline, pass -p\n  $ bk build view -p monolith\n\n  # To view a specific build\n  $ bk build view 429\n\n  # Add -w to any command to open the build in your web browser instead\n  $ bk build view -w 429\n\n  # To view the most recent build on feature-x branch\n  $ bk build view -b feature-y\n\n  # You can filter by a user name or id\n  $ bk build view -u \"alice\"\n\n  # A shortcut to view your builds is --mine\n  $ bk build view --mine\n\n  # Filter to only show failed and broken jobs\n  $ bk build view -s failed,broken\n\n  # You can combine most of these flags\n  # To view most recent build by greg on the deploy-pipeline\n  $ bk build view -p deploy-pipeline -u \"greg\"`\n}\n\nfunc (c *ViewCmd) buildGetOptions() *buildkite.BuildGetOptions {\n\tif len(c.JobStates) > 0 {\n\t\treturn &buildkite.BuildGetOptions{JobStates: c.JobStates}\n\t}\n\treturn nil\n}\n\nfunc (c *ViewCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tvar opts view.ViewOptions\n\topts.Pipeline = c.Pipeline\n\topts.Web = c.Web\n\n\t// Resolve pipeline first\n\tpipelineRes := pipelineResolver.NewAggregateResolver(\n\t\tpipelineResolver.ResolveFromFlag(opts.Pipeline, f.Config),\n\t\tpipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)),\n\t\tpipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOneWithFactory(f))),\n\t)\n\n\t// Resolve build options\n\toptionsResolver := options.AggregateResolver{\n\t\toptions.ResolveBranchFromFlag(c.Branch),\n\t\toptions.ResolveBranchFromRepository(f.GitRepository),\n\t}.WithResolverWhen(\n\t\tc.User != \"\",\n\t\toptions.ResolveUserFromFlag(c.User),\n\t).WithResolverWhen(\n\t\tc.Mine || c.User == \"\",\n\t\toptions.ResolveCurrentUser(ctx, f),\n\t)\n\n\t// Resolve build\n\targs := []string{}\n\tif c.BuildNumber != \"\" {\n\t\targs = []string{c.BuildNumber}\n\t}\n\tbuildRes := buildResolver.NewAggregateResolver(\n\t\tbuildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config),\n\t\tbuildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...),\n\t)\n\n\tbld, err := buildRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif bld == nil {\n\t\treturn output.WriteTextOrStructured(os.Stdout, format, nil, \"No build found.\")\n\t}\n\n\topts.Organization = bld.Organization\n\topts.Pipeline = bld.Pipeline\n\topts.BuildNumber = bld.BuildNumber\n\n\tif err := opts.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\tif opts.Web {\n\t\tbuildURL := fmt.Sprintf(\"https://buildkite.com/%s/%s/builds/%d\",\n\t\t\topts.Organization, opts.Pipeline, opts.BuildNumber)\n\t\tfmt.Printf(\"Opening %s in your browser\\n\", buildURL)\n\t\treturn browser.OpenURL(buildURL)\n\t}\n\n\tvar build buildkite.Build\n\tvar artifacts []buildkite.Artifact\n\tvar annotations []buildkite.Annotation\n\tvar wg sync.WaitGroup\n\tvar mu sync.Mutex\n\n\tif err = bkIO.SpinWhile(f, \"Loading build information\", func() error {\n\t\tvar fetchErr error\n\t\twg.Add(3)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tvar apiErr error\n\t\t\tbuild, _, apiErr = f.RestAPIClient.Builds.Get(\n\t\t\t\tctx,\n\t\t\t\topts.Organization,\n\t\t\t\topts.Pipeline,\n\t\t\t\tfmt.Sprint(opts.BuildNumber),\n\t\t\t\tc.buildGetOptions(),\n\t\t\t)\n\t\t\tif apiErr != nil {\n\t\t\t\tmu.Lock()\n\t\t\t\tfetchErr = apiErr\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tvar apiErr error\n\t\t\tartifacts, _, apiErr = f.RestAPIClient.Artifacts.ListByBuild(\n\t\t\t\tctx,\n\t\t\t\topts.Organization,\n\t\t\t\topts.Pipeline,\n\t\t\t\tfmt.Sprint(opts.BuildNumber),\n\t\t\t\tnil,\n\t\t\t)\n\t\t\tif apiErr != nil {\n\t\t\t\tmu.Lock()\n\t\t\t\tfetchErr = apiErr\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tvar apiErr error\n\t\t\tannotations, _, apiErr = f.RestAPIClient.Annotations.ListByBuild(\n\t\t\t\tctx,\n\t\t\t\topts.Organization,\n\t\t\t\topts.Pipeline,\n\t\t\t\tfmt.Sprint(opts.BuildNumber),\n\t\t\t\tnil,\n\t\t\t)\n\t\t\tif apiErr != nil {\n\t\t\t\tmu.Lock()\n\t\t\t\tfetchErr = apiErr\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\t\t}()\n\n\t\twg.Wait()\n\t\treturn fetchErr\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\t// Create a combined view for JSON/YAML output\n\ttype BuildOutput struct {\n\t\tbuildkite.Build\n\t\tArtifacts   []buildkite.Artifact   `json:\"artifacts,omitempty\"`\n\t\tAnnotations []buildkite.Annotation `json:\"annotations,omitempty\"`\n\t}\n\n\tbuildOutput := output.Viewable[BuildOutput]{\n\t\tData: BuildOutput{\n\t\t\tBuild:       build,\n\t\t\tArtifacts:   artifacts,\n\t\t\tAnnotations: annotations,\n\t\t},\n\t\tRender: func(b BuildOutput) string {\n\t\t\treturn view.NewBuildView(&b.Build, b.Artifacts, b.Annotations, opts.Organization, opts.Pipeline).Render()\n\t\t},\n\t}\n\n\tif format == output.FormatText {\n\t\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\t\tdefer func() { _ = cleanup() }()\n\n\t\t_, err := fmt.Fprint(writer, buildOutput.TextOutput())\n\t\treturn err\n\t}\n\n\treturn output.Write(os.Stdout, buildOutput, format)\n}\n"
  },
  {
    "path": "cmd/build/view_test.go",
    "content": "package build\n\nimport (\n\t\"testing\"\n)\n\nfunc TestViewCmd_BuildGetOptions_WithJobStates(t *testing.T) {\n\tcmd := &ViewCmd{\n\t\tJobStates: []string{\"failed\", \"broken\"},\n\t}\n\n\topts := cmd.buildGetOptions()\n\tif opts == nil {\n\t\tt.Fatal(\"Expected non-nil BuildGetOptions\")\n\t\treturn\n\t}\n\n\tif len(opts.JobStates) != 2 {\n\t\tt.Fatalf(\"Expected 2 job states, got %d\", len(opts.JobStates))\n\t}\n\n\tif opts.JobStates[0] != \"failed\" {\n\t\tt.Errorf(\"Expected first state to be 'failed', got %q\", opts.JobStates[0])\n\t}\n\n\tif opts.JobStates[1] != \"broken\" {\n\t\tt.Errorf(\"Expected second state to be 'broken', got %q\", opts.JobStates[1])\n\t}\n}\n\nfunc TestViewCmd_BuildGetOptions_Empty(t *testing.T) {\n\tcmd := &ViewCmd{}\n\n\topts := cmd.buildGetOptions()\n\tif opts != nil {\n\t\tt.Errorf(\"Expected nil BuildGetOptions when no job states, got %+v\", opts)\n\t}\n}\n\nfunc TestViewCmd_BuildGetOptions_SingleState(t *testing.T) {\n\tcmd := &ViewCmd{\n\t\tJobStates: []string{\"running\"},\n\t}\n\n\topts := cmd.buildGetOptions()\n\tif opts == nil {\n\t\tt.Fatal(\"Expected non-nil BuildGetOptions\")\n\t\treturn\n\t}\n\n\tif len(opts.JobStates) != 1 {\n\t\tt.Fatalf(\"Expected 1 job state, got %d\", len(opts.JobStates))\n\t}\n\n\tif opts.JobStates[0] != \"running\" {\n\t\tt.Errorf(\"Expected state to be 'running', got %q\", opts.JobStates[0])\n\t}\n}\n"
  },
  {
    "path": "cmd/build/watch.go",
    "content": "package build\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\tbuildResolver \"github.com/buildkite/cli/v3/internal/build/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/build/resolver/options\"\n\t\"github.com/buildkite/cli/v3/internal/build/view/shared\"\n\t\"github.com/buildkite/cli/v3/internal/build/watch\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\tpkgValidation \"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\t\"github.com/mattn/go-isatty\"\n)\n\ntype WatchCmd struct {\n\tBuildNumber string `arg:\"\" optional:\"\" help:\"Build number to watch (omit for most recent build)\"`\n\tPipeline    string `help:\"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.\" short:\"p\"`\n\tBranch      string `help:\"The branch to watch builds for.\" short:\"b\"`\n\tInterval    int    `help:\"Polling interval in seconds\" default:\"1\"`\n}\n\nfunc (c *WatchCmd) Help() string {\n\treturn `\nExamples:\n  # Watch the most recent build for the current branch\n  $ bk build watch --pipeline my-pipeline\n\n  # Watch a specific build\n  $ bk build watch 429 --pipeline my-pipeline\n\n  # Watch the most recent build on a specific branch\n  $ bk build watch -b feature-x --pipeline my-pipeline\n\n  # Watch a build on a specific pipeline\n  $ bk build watch --pipeline my-pipeline\n\n  # Set a custom polling interval (in seconds)\n  $ bk build watch --interval 5 --pipeline my-pipeline`\n}\n\nfunc (c *WatchCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := pkgValidation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\ttty := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())\n\n\t// Validate command options\n\tv := validation.New()\n\tv.AddRule(\"Interval\", validation.MinValue(1))\n\tif c.Pipeline != \"\" {\n\t\tv.AddRule(\"Pipeline\", validation.Slug)\n\t}\n\tif err := v.Validate(map[string]interface{}{\n\t\t\"Pipeline\": c.Pipeline,\n\t\t\"Interval\": c.Interval,\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\tpipelineRes := pipelineResolver.NewAggregateResolver(\n\t\tpipelineResolver.ResolveFromFlag(c.Pipeline, f.Config),\n\t\tpipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)),\n\t\tpipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOneWithFactory(f))),\n\t)\n\n\toptionsResolver := options.AggregateResolver{\n\t\toptions.ResolveBranchFromFlag(c.Branch),\n\t\toptions.ResolveBranchFromRepository(f.GitRepository),\n\t}\n\n\targs := []string{}\n\tif c.BuildNumber != \"\" {\n\t\targs = []string{c.BuildNumber}\n\t}\n\tbuildRes := buildResolver.NewAggregateResolver(\n\t\tbuildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config),\n\t\tbuildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...),\n\t)\n\n\tbld, err := buildRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif bld == nil {\n\t\treturn fmt.Errorf(\"no running builds found\")\n\t}\n\n\tfmt.Printf(\"Watching build %d on %s/%s\\n\", bld.BuildNumber, bld.Organization, bld.Pipeline)\n\n\tinterval := time.Duration(c.Interval) * time.Second\n\n\t_, err = watch.WatchBuild(ctx, f.RestAPIClient, bld.Organization, bld.Pipeline, bld.BuildNumber, interval, func(b buildkite.Build) error {\n\t\tsummary := shared.BuildSummaryWithJobs(&b, bld.Organization, bld.Pipeline)\n\t\tif tty {\n\t\t\tfmt.Print(\"\\033[H\\033[2J\")\n\t\t\tfmt.Printf(\"%s\\n\", summary)\n\t\t} else {\n\t\t\tfmt.Printf(\"[%s] %s\\n\", time.Now().Format(time.RFC3339), summary)\n\t\t}\n\t\treturn nil\n\t})\n\tif errors.Is(err, context.Canceled) {\n\t\treturn nil\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "cmd/cluster/cluster_test.go",
    "content": "package cluster\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestListClusters(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"fetches clusters through API\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tclusters := []buildkite.Cluster{\n\t\t\t{\n\t\t\t\tID:          \"cluster-1\",\n\t\t\t\tName:        \"Production\",\n\t\t\t\tDescription: \"Production cluster\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tID:          \"cluster-2\",\n\t\t\t\tName:        \"Staging\",\n\t\t\t\tDescription: \"Staging cluster\",\n\t\t\t},\n\t\t}\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != \"GET\" {\n\t\t\t\tt.Errorf(\"expected GET, got %s\", r.Method)\n\t\t\t}\n\t\t\tif !strings.Contains(r.URL.Path, \"/clusters\") {\n\t\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t\t}\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(clusters)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tresult, _, err := client.Clusters.List(context.Background(), \"test-org\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(result) != 2 {\n\t\t\tt.Fatalf(\"expected 2 clusters, got %d\", len(result))\n\t\t}\n\n\t\tif result[0].Name != \"Production\" {\n\t\t\tt.Errorf(\"expected name 'Production', got %q\", result[0].Name)\n\t\t}\n\n\t\tif result[1].ID != \"cluster-2\" {\n\t\t\tt.Errorf(\"expected ID 'cluster-2', got %q\", result[1].ID)\n\t\t}\n\t})\n\n\tt.Run(\"empty result returns empty slice\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode([]buildkite.Cluster{})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tresult, _, err := client.Clusters.List(context.Background(), \"test-org\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(result) != 0 {\n\t\t\tt.Errorf(\"expected 0 clusters, got %d\", len(result))\n\t\t}\n\t})\n}\n\nfunc TestGetCluster(t *testing.T) {\n\tt.Parallel()\n\n\tcluster := buildkite.Cluster{\n\t\tID:          \"cluster-1\",\n\t\tName:        \"Production\",\n\t\tDescription: \"Production cluster\",\n\t\tColor:       \"#FF0000\",\n\t\tEmoji:       \":rocket:\",\n\t}\n\n\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != \"GET\" {\n\t\t\tt.Errorf(\"expected GET, got %s\", r.Method)\n\t\t}\n\t\tif !strings.Contains(r.URL.Path, \"/clusters/cluster-1\") {\n\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(w).Encode(cluster)\n\t}))\n\tdefer s.Close()\n\n\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tresult, _, err := client.Clusters.Get(context.Background(), \"test-org\", \"cluster-1\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif result.Name != \"Production\" {\n\t\tt.Errorf(\"expected name 'Production', got %q\", result.Name)\n\t}\n\n\tif result.Color != \"#FF0000\" {\n\t\tt.Errorf(\"expected color '#FF0000', got %q\", result.Color)\n\t}\n}\n\nfunc TestCreateCluster(t *testing.T) {\n\tt.Parallel()\n\n\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != \"POST\" {\n\t\t\tt.Errorf(\"expected POST, got %s\", r.Method)\n\t\t}\n\t\tif !strings.Contains(r.URL.Path, \"/clusters\") {\n\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t}\n\n\t\tvar input buildkite.ClusterCreate\n\t\tif err := json.NewDecoder(r.Body).Decode(&input); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif input.Name != \"New Cluster\" {\n\t\t\tt.Errorf(\"expected name 'New Cluster', got %q\", input.Name)\n\t\t}\n\n\t\tif input.Description != \"A brand new cluster\" {\n\t\t\tt.Errorf(\"expected description 'A brand new cluster', got %q\", input.Description)\n\t\t}\n\n\t\tif input.Color != \"#FF0000\" {\n\t\t\tt.Errorf(\"expected color '#FF0000', got %q\", input.Color)\n\t\t}\n\n\t\tif input.Emoji != \":rocket:\" {\n\t\t\tt.Errorf(\"expected emoji ':rocket:', got %q\", input.Emoji)\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(http.StatusCreated)\n\t\tjson.NewEncoder(w).Encode(buildkite.Cluster{\n\t\t\tID:          \"new-cluster-id\",\n\t\t\tName:        input.Name,\n\t\t\tDescription: input.Description,\n\t\t\tColor:       input.Color,\n\t\t\tEmoji:       input.Emoji,\n\t\t})\n\t}))\n\tdefer s.Close()\n\n\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tresult, _, err := client.Clusters.Create(context.Background(), \"test-org\", buildkite.ClusterCreate{\n\t\tName:        \"New Cluster\",\n\t\tDescription: \"A brand new cluster\",\n\t\tColor:       \"#FF0000\",\n\t\tEmoji:       \":rocket:\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif result.ID != \"new-cluster-id\" {\n\t\tt.Errorf(\"expected ID 'new-cluster-id', got %q\", result.ID)\n\t}\n\n\tif result.Name != \"New Cluster\" {\n\t\tt.Errorf(\"expected name 'New Cluster', got %q\", result.Name)\n\t}\n}\n\nfunc TestUpdateCluster(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"updates cluster metadata\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != \"PATCH\" {\n\t\t\t\tt.Errorf(\"expected PATCH, got %s\", r.Method)\n\t\t\t}\n\t\t\tif !strings.Contains(r.URL.Path, \"/clusters/cluster-1\") {\n\t\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t\t}\n\n\t\t\tvar input buildkite.ClusterUpdate\n\t\t\tif err := json.NewDecoder(r.Body).Decode(&input); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif input.Name != \"Updated Name\" {\n\t\t\t\tt.Errorf(\"expected name 'Updated Name', got %q\", input.Name)\n\t\t\t}\n\n\t\t\tif input.Description != \"Updated description\" {\n\t\t\t\tt.Errorf(\"expected description 'Updated description', got %q\", input.Description)\n\t\t\t}\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(buildkite.Cluster{\n\t\t\t\tID:          \"cluster-1\",\n\t\t\t\tName:        input.Name,\n\t\t\t\tDescription: input.Description,\n\t\t\t})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tresult, _, err := client.Clusters.Update(context.Background(), \"test-org\", \"cluster-1\", buildkite.ClusterUpdate{\n\t\t\tName:        \"Updated Name\",\n\t\t\tDescription: \"Updated description\",\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif result.Name != \"Updated Name\" {\n\t\t\tt.Errorf(\"expected name 'Updated Name', got %q\", result.Name)\n\t\t}\n\n\t\tif result.Description != \"Updated description\" {\n\t\t\tt.Errorf(\"expected description 'Updated description', got %q\", result.Description)\n\t\t}\n\t})\n\n\tt.Run(\"updates default queue\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != \"PATCH\" {\n\t\t\t\tt.Errorf(\"expected PATCH, got %s\", r.Method)\n\t\t\t}\n\n\t\t\tvar input buildkite.ClusterUpdate\n\t\t\tif err := json.NewDecoder(r.Body).Decode(&input); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif input.DefaultQueueID != \"queue-123\" {\n\t\t\t\tt.Errorf(\"expected default_queue_id 'queue-123', got %q\", input.DefaultQueueID)\n\t\t\t}\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(buildkite.Cluster{\n\t\t\t\tID:             \"cluster-1\",\n\t\t\t\tName:           \"Production\",\n\t\t\t\tDefaultQueueID: input.DefaultQueueID,\n\t\t\t})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tresult, _, err := client.Clusters.Update(context.Background(), \"test-org\", \"cluster-1\", buildkite.ClusterUpdate{\n\t\t\tDefaultQueueID: \"queue-123\",\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif result.DefaultQueueID != \"queue-123\" {\n\t\t\tt.Errorf(\"expected default_queue_id 'queue-123', got %q\", result.DefaultQueueID)\n\t\t}\n\t})\n}\n\nfunc TestDeleteCluster(t *testing.T) {\n\tt.Parallel()\n\n\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != \"DELETE\" {\n\t\t\tt.Errorf(\"expected DELETE, got %s\", r.Method)\n\t\t}\n\t\tif !strings.Contains(r.URL.Path, \"/clusters/cluster-to-delete\") {\n\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t}\n\t\tw.WriteHeader(http.StatusNoContent)\n\t}))\n\tdefer s.Close()\n\n\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t_, err = client.Clusters.Delete(context.Background(), \"test-org\", \"cluster-to-delete\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestUpdateCmdValidate(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname    string\n\t\tcmd     UpdateCmd\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"no flags set\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"cluster-1\"},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"only name\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"cluster-1\", Name: \"New Name\"},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"only description\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"cluster-1\", Description: \"New description\"},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"only emoji\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"cluster-1\", Emoji: \":rocket:\"},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"only color\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"cluster-1\", Color: \"#FF0000\"},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"only default-queue-id\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"cluster-1\", DefaultQueueID: \"queue-123\"},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"multiple fields\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"cluster-1\", Name: \"New Name\", Description: \"New desc\", Color: \"#00FF00\"},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\terr := tt.cmd.Validate()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRenderClusterText(t *testing.T) {\n\tt.Parallel()\n\n\tts := buildkite.Timestamp{}\n\tcluster := buildkite.Cluster{\n\t\tID:          \"cluster-123\",\n\t\tGraphQLID:   \"graphql-123\",\n\t\tName:        \"Production\",\n\t\tDescription: \"Production cluster\",\n\t\tColor:       \"#FF0000\",\n\t\tEmoji:       \":rocket:\",\n\t\tWebURL:      \"https://buildkite.com/orgs/test-org/clusters/cluster-123\",\n\t\tCreatedBy: buildkite.ClusterCreator{\n\t\t\tID:   \"user-1\",\n\t\t\tName: \"Test User\",\n\t\t},\n\t\tCreatedAt: &ts,\n\t}\n\n\tresult := renderClusterText(cluster)\n\n\texpectedStrings := []string{\n\t\t\"Viewing Production\",\n\t\t\"cluster-123\",\n\t\t\"Production cluster\",\n\t\t\"#FF0000\",\n\t\t\":rocket:\",\n\t\t\"Test User\",\n\t}\n\n\tfor _, expected := range expectedStrings {\n\t\tif !strings.Contains(result, expected) {\n\t\t\tt.Errorf(\"expected output to contain %q, got:\\n%s\", expected, result)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cmd/cluster/create.go",
    "content": "package cluster\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype CreateCmd struct {\n\tName        string `help:\"The name of the cluster\" required:\"\"`\n\tDescription string `help:\"A description of the cluster\" optional:\"\"`\n\tEmoji       string `help:\"An emoji for the cluster (e.g. :rocket:)\" optional:\"\"`\n\tColor       string `help:\"A color hex code for the cluster (e.g. #FF0000)\" optional:\"\"`\n\toutput.OutputFlags\n}\n\nfunc (c *CreateCmd) Help() string {\n\treturn `\nCreate a new cluster in the organization.\n\nExamples:\n  # Create a cluster with just a name\n  $ bk cluster create --name \"My Cluster\"\n\n  # Create a cluster with all fields\n  $ bk cluster create --name \"My Cluster\" --description \"Runs production workloads\" --emoji :rocket: --color \"#FF0000\"\n\n  # Create a cluster and output as JSON\n  $ bk cluster create --name \"My Cluster\" -o json\n`\n}\n\nfunc (c *CreateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tinput := buildkite.ClusterCreate{\n\t\tName:        c.Name,\n\t\tDescription: c.Description,\n\t\tEmoji:       c.Emoji,\n\t\tColor:       c.Color,\n\t}\n\n\tvar cluster buildkite.Cluster\n\tif err = bkIO.SpinWhile(f, \"Creating cluster\", func() error {\n\t\tvar apiErr error\n\t\tcluster, _, apiErr = f.RestAPIClient.Clusters.Create(ctx, f.Config.OrganizationSlug(), input)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error creating cluster: %v\", err)\n\t}\n\n\tclusterView := output.Viewable[buildkite.Cluster]{\n\t\tData:   cluster,\n\t\tRender: renderClusterText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, clusterView, format)\n\t}\n\n\tfmt.Fprintf(os.Stdout, \"Cluster %s created successfully\\n\\n\", cluster.Name)\n\treturn output.Write(os.Stdout, clusterView, format)\n}\n"
  },
  {
    "path": "cmd/cluster/delete.go",
    "content": "package cluster\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n)\n\ntype DeleteCmd struct {\n\tClusterUUID string `arg:\"\" help:\"Cluster UUID to delete\" name:\"cluster-uuid\"`\n}\n\nfunc (c *DeleteCmd) Help() string {\n\treturn `\nDelete a cluster from the organization.\n\nYou will be prompted to confirm deletion unless --yes is set.\n\nExamples:\n  # Delete a cluster (with confirmation prompt)\n  $ bk cluster delete my-cluster-uuid\n\n  # Delete a cluster without confirmation\n  $ bk cluster delete my-cluster-uuid --yes\n`\n}\n\nfunc (c *DeleteCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tconfirmed, err := bkIO.Confirm(f, fmt.Sprintf(\"Are you sure you want to delete cluster %s?\", c.ClusterUUID))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !confirmed {\n\t\tfmt.Fprintln(os.Stderr, \"Deletion cancelled.\")\n\t\treturn nil\n\t}\n\n\tif err = bkIO.SpinWhile(f, \"Deleting cluster\", func() error {\n\t\t_, err = f.RestAPIClient.Clusters.Delete(ctx, f.Config.OrganizationSlug(), c.ClusterUUID)\n\t\treturn err\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error deleting cluster: %v\", err)\n\t}\n\n\tfmt.Fprintln(os.Stderr, \"Cluster deleted successfully.\")\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/cluster/list.go",
    "content": "package cluster\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"sync\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/internal/cluster\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype ListCmd struct {\n\toutput.OutputFlags\n}\n\nfunc (c *ListCmd) Help() string {\n\treturn `\nList the clusters for an organization.\n\nExamples:\n  # List all clusters\n  $ bk cluster list\n\n  # List clusters in JSON format\n  $ bk cluster list -o json\n`\n}\n\nfunc (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tclusters, err := listClusters(ctx, f)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, clusters, format)\n\t}\n\n\tsummary := cluster.ClusterViewTable(clusters...)\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\tfmt.Fprintf(writer, \"%v\\n\", summary)\n\n\treturn nil\n}\n\nfunc listClusters(ctx context.Context, f *factory.Factory) ([]buildkite.Cluster, error) {\n\tvar clusters []buildkite.Cluster\n\tvar err error\n\n\tif err = bkIO.SpinWhile(f, \"Loading clusters information\", func() error {\n\t\tvar apiErr error\n\t\tclusters, _, apiErr = f.RestAPIClient.Clusters.List(ctx, f.Config.OrganizationSlug(), nil)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn nil, fmt.Errorf(\"error fetching cluster list: %v\", err)\n\t}\n\n\tif len(clusters) < 1 {\n\t\treturn nil, errors.New(\"no clusters found in organization\")\n\t}\n\n\tclusterList := make([]buildkite.Cluster, len(clusters))\n\tvar wg sync.WaitGroup\n\terrChan := make(chan error, len(clusters))\n\tfor i, c := range clusters {\n\t\twg.Add(1)\n\t\tgo func(i int, c buildkite.Cluster) {\n\t\t\tdefer wg.Done()\n\t\t\tclusterList[i] = buildkite.Cluster{\n\t\t\t\tColor:           c.Color,\n\t\t\t\tCreatedAt:       c.CreatedAt,\n\t\t\t\tCreatedBy:       c.CreatedBy,\n\t\t\t\tDefaultQueueID:  c.DefaultQueueID,\n\t\t\t\tDefaultQueueURL: c.DefaultQueueURL,\n\t\t\t\tDescription:     c.Description,\n\t\t\t\tEmoji:           c.Emoji,\n\t\t\t\tGraphQLID:       c.GraphQLID,\n\t\t\t\tID:              c.ID,\n\t\t\t\tName:            c.Name,\n\t\t\t\tQueuesURL:       c.QueuesURL,\n\t\t\t\tURL:             c.URL,\n\t\t\t\tWebURL:          c.WebURL,\n\t\t\t}\n\t\t}(i, c)\n\t}\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(errChan)\n\t}()\n\n\tfor err := range errChan {\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn clusterList, nil\n}\n"
  },
  {
    "path": "cmd/cluster/update.go",
    "content": "package cluster\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype UpdateCmd struct {\n\tClusterUUID    string `arg:\"\" help:\"Cluster UUID to update\" name:\"cluster-uuid\"`\n\tName           string `help:\"New name for the cluster\" optional:\"\"`\n\tDescription    string `help:\"New description for the cluster\" optional:\"\"`\n\tEmoji          string `help:\"New emoji for the cluster (e.g. :rocket:)\" optional:\"\"`\n\tColor          string `help:\"New color hex code for the cluster (e.g. #FF0000)\" optional:\"\"`\n\tDefaultQueueID string `help:\"UUID of the queue to set as the default\" optional:\"\" name:\"default-queue-id\"`\n\toutput.OutputFlags\n}\n\nfunc (c *UpdateCmd) Help() string {\n\treturn `\nUpdate a cluster's settings.\n\nAt least one of --name, --description, --emoji, --color, or --default-queue-id must be provided.\n\nExamples:\n  # Update a cluster's name\n  $ bk cluster update my-cluster-uuid --name \"New Name\"\n\n  # Update description and color\n  $ bk cluster update my-cluster-uuid --description \"Updated description\" --color \"#00FF00\"\n\n  # Set the default queue\n  $ bk cluster update my-cluster-uuid --default-queue-id my-queue-uuid\n\n  # Output the updated cluster as JSON\n  $ bk cluster update my-cluster-uuid --name \"New Name\" -o json\n`\n}\n\nfunc (c *UpdateCmd) Validate() error {\n\tif c.Name == \"\" && c.Description == \"\" && c.Emoji == \"\" && c.Color == \"\" && c.DefaultQueueID == \"\" {\n\t\treturn fmt.Errorf(\"at least one of --name, --description, --emoji, --color, or --default-queue-id must be provided\")\n\t}\n\treturn nil\n}\n\nfunc (c *UpdateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tinput := buildkite.ClusterUpdate{\n\t\tName:           c.Name,\n\t\tDescription:    c.Description,\n\t\tEmoji:          c.Emoji,\n\t\tColor:          c.Color,\n\t\tDefaultQueueID: c.DefaultQueueID,\n\t}\n\n\tvar cluster buildkite.Cluster\n\tif err = bkIO.SpinWhile(f, \"Updating cluster\", func() error {\n\t\tvar apiErr error\n\t\tcluster, _, apiErr = f.RestAPIClient.Clusters.Update(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, input)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error updating cluster: %v\", err)\n\t}\n\n\tclusterView := output.Viewable[buildkite.Cluster]{\n\t\tData:   cluster,\n\t\tRender: renderClusterText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, clusterView, format)\n\t}\n\n\tfmt.Fprintf(os.Stdout, \"Cluster %s updated successfully\\n\\n\", cluster.Name)\n\treturn output.Write(os.Stdout, clusterView, format)\n}\n"
  },
  {
    "path": "cmd/cluster/view.go",
    "content": "package cluster\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype ViewCmd struct {\n\tClusterUUID string `arg:\"\" help:\"Cluster UUID to view\" name:\"cluster-uuid\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ViewCmd) Help() string {\n\treturn `\nIt accepts cluster UUID.\n\nExamples:\n  # View a cluster\n  $ bk cluster view my-cluster-uuid\n\n  # View cluster in JSON format\n  $ bk cluster view my-cluster-uuid -o json\n`\n}\n\nfunc (c *ViewCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tvar cluster buildkite.Cluster\n\tif err = bkIO.SpinWhile(f, \"Loading cluster information\", func() error {\n\t\tvar apiErr error\n\t\tcluster, _, apiErr = f.RestAPIClient.Clusters.Get(ctx, f.Config.OrganizationSlug(), c.ClusterUUID)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tclusterView := output.Viewable[buildkite.Cluster]{\n\t\tData:   cluster,\n\t\tRender: renderClusterText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, clusterView, format)\n\t}\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\treturn output.Write(writer, clusterView, format)\n}\n\nfunc renderClusterText(c buildkite.Cluster) string {\n\trows := [][]string{\n\t\t{\"Description\", output.ValueOrDash(c.Description)},\n\t\t{\"Color\", output.ValueOrDash(c.Color)},\n\t\t{\"Emoji\", output.ValueOrDash(c.Emoji)},\n\t\t{\"ID\", output.ValueOrDash(c.ID)},\n\t\t{\"GraphQL ID\", output.ValueOrDash(c.GraphQLID)},\n\t\t{\"Default Queue ID\", output.ValueOrDash(c.DefaultQueueID)},\n\t\t{\"Web URL\", output.ValueOrDash(c.WebURL)},\n\t\t{\"API URL\", output.ValueOrDash(c.URL)},\n\t\t{\"Queues URL\", output.ValueOrDash(c.QueuesURL)},\n\t\t{\"Queue URL\", output.ValueOrDash(c.DefaultQueueURL)},\n\t}\n\n\tif c.CreatedBy.ID != \"\" {\n\t\trows = append(\n\t\t\trows,\n\t\t\t[]string{\"Created By Name\", output.ValueOrDash(c.CreatedBy.Name)},\n\t\t\t[]string{\"Created By Email\", output.ValueOrDash(c.CreatedBy.Email)},\n\t\t\t[]string{\"Created By ID\", output.ValueOrDash(c.CreatedBy.ID)},\n\t\t)\n\t}\n\n\tif c.CreatedAt != nil {\n\t\trows = append(rows, []string{\"Created At\", c.CreatedAt.Format(time.RFC3339)})\n\t}\n\n\tvar sb strings.Builder\n\tfmt.Fprintf(&sb, \"Viewing %s\\n\\n\", output.ValueOrDash(c.Name))\n\n\ttable := output.Table(\n\t\t[]string{\"Field\", \"Value\"},\n\t\trows,\n\t\tmap[string]string{\"field\": \"dim\", \"value\": \"italic\"},\n\t)\n\n\tsb.WriteString(table)\n\treturn sb.String()\n}\n"
  },
  {
    "path": "cmd/config/config.go",
    "content": "// Package config provides commands for managing CLI configuration\npackage config\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n)\n\n// ConfigCmd is the root command for managing CLI configuration\ntype ConfigCmd struct {\n\tList  ListCmd  `cmd:\"\" help:\"List configuration values.\" aliases:\"ls\"`\n\tGet   GetCmd   `cmd:\"\" help:\"Get a configuration value.\"`\n\tSet   SetCmd   `cmd:\"\" help:\"Set a configuration value.\"`\n\tUnset UnsetCmd `cmd:\"\" help:\"Remove a configuration value.\"`\n}\n\nfunc (c ConfigCmd) Help() string {\n\treturn `Manage CLI configuration settings.\n\nConfiguration is stored in two locations:\n  User config:   ~/.config/bk.yaml (global defaults)\n  Local config:  .bk.yaml (repo-specific overrides)\n\nPrecedence: Environment variable > Local config > User config > Default\n\nExamples:\n  $ bk config list                       # Show all config values\n  $ bk config get output_format          # Get a specific value\n  $ bk config set output_format yaml     # Set default output to YAML\n  $ bk config set no_pager true --local  # Disable pager for this repo\n  $ bk config unset pager                # Reset pager to default`\n}\n\n// ConfigKey represents a valid configuration key\ntype ConfigKey string\n\nconst (\n\tKeySelectedOrg  ConfigKey = \"selected_org\"\n\tKeyOutputFormat ConfigKey = \"output_format\"\n\tKeyNoPager      ConfigKey = \"no_pager\"\n\tKeyQuiet        ConfigKey = \"quiet\"\n\tKeyNoInput      ConfigKey = \"no_input\"\n\tKeyPager        ConfigKey = \"pager\"\n\tKeyTelemetry    ConfigKey = \"telemetry\"\n\tKeyExperiments  ConfigKey = \"experiments\"\n)\n\n// AllKeys returns all valid configuration keys\nfunc AllKeys() []ConfigKey {\n\treturn []ConfigKey{\n\t\tKeySelectedOrg,\n\t\tKeyOutputFormat,\n\t\tKeyNoPager,\n\t\tKeyQuiet,\n\t\tKeyNoInput,\n\t\tKeyPager,\n\t\tKeyTelemetry,\n\t\tKeyExperiments,\n\t}\n}\n\n// ValidateKey checks if a key is valid\nfunc ValidateKey(key string) (ConfigKey, error) {\n\tk := ConfigKey(key)\n\tif slices.Contains(AllKeys(), k) {\n\t\treturn k, nil\n\t}\n\treturn \"\", fmt.Errorf(\"unknown config key: %s\\nvalid keys: %v\", key, AllKeys())\n}\n\n// IsLocalOnly returns true if the key can only be set in user config\nfunc (k ConfigKey) IsLocalOnly() bool {\n\treturn false\n}\n\n// IsUserOnly returns true if the key can only be set in user config\nfunc (k ConfigKey) IsUserOnly() bool {\n\tswitch k {\n\tcase KeyNoInput, KeyPager, KeyTelemetry, KeyExperiments:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// IsBool returns true if the key is a boolean value\nfunc (k ConfigKey) IsBool() bool {\n\tswitch k {\n\tcase KeyNoPager, KeyQuiet, KeyNoInput, KeyTelemetry:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// ValidValues returns valid values for enum keys, or nil if any value is valid\nfunc (k ConfigKey) ValidValues() []string {\n\tswitch k {\n\tcase KeyOutputFormat:\n\t\treturn []string{\"json\", \"yaml\", \"text\"}\n\tcase KeyNoPager, KeyQuiet, KeyNoInput, KeyTelemetry:\n\t\treturn []string{\"true\", \"false\"}\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// parseBoolOrDefault parses a boolean string, returning the default for empty strings\nfunc parseBoolOrDefault(value string, defaultVal bool) (bool, error) {\n\tif value == \"\" {\n\t\treturn defaultVal, nil\n\t}\n\treturn strconv.ParseBool(value)\n}\n\nfunc SetConfigValue(conf *config.Config, key ConfigKey, value string, local bool) error {\n\tswitch key {\n\tcase KeySelectedOrg:\n\t\treturn conf.SelectOrganization(value, local)\n\tcase KeyOutputFormat:\n\t\treturn conf.SetOutputFormat(value, local)\n\tcase KeyNoPager:\n\t\tv, err := parseBoolOrDefault(value, false)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid boolean value %q: %w\", value, err)\n\t\t}\n\t\treturn conf.SetNoPager(v, local)\n\tcase KeyQuiet:\n\t\tv, err := parseBoolOrDefault(value, false)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid boolean value %q: %w\", value, err)\n\t\t}\n\t\treturn conf.SetQuiet(v, local)\n\tcase KeyNoInput:\n\t\tv, err := parseBoolOrDefault(value, false)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid boolean value %q: %w\", value, err)\n\t\t}\n\t\treturn conf.SetNoInput(v)\n\tcase KeyPager:\n\t\treturn conf.SetPager(value)\n\tcase KeyTelemetry:\n\t\tv, err := parseBoolOrDefault(value, true)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid boolean value %q: %w\", value, err)\n\t\t}\n\t\treturn conf.SetTelemetry(v)\n\tcase KeyExperiments:\n\t\treturn conf.SetExperiments(value)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/config/config_test.go",
    "content": "package config\n\nimport (\n\t\"testing\"\n)\n\nfunc TestValidateKey(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"valid keys\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvalidKeys := []string{\n\t\t\t\"selected_org\",\n\t\t\t\"output_format\",\n\t\t\t\"no_pager\",\n\t\t\t\"quiet\",\n\t\t\t\"no_input\",\n\t\t\t\"pager\",\n\t\t\t\"experiments\",\n\t\t}\n\n\t\tfor _, key := range validKeys {\n\t\t\tt.Run(key, func(t *testing.T) {\n\t\t\t\tgot, err := ValidateKey(key)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"ValidateKey(%q) returned error: %v\", key, err)\n\t\t\t\t}\n\t\t\t\tif string(got) != key {\n\t\t\t\t\tt.Errorf(\"ValidateKey(%q) = %q, want %q\", key, got, key)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"invalid key\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t_, err := ValidateKey(\"invalid_key\")\n\t\tif err == nil {\n\t\t\tt.Error(\"ValidateKey(\\\"invalid_key\\\") expected error, got nil\")\n\t\t}\n\t})\n}\n\nfunc TestConfigKeyIsBool(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tkey    ConfigKey\n\t\tisBool bool\n\t}{\n\t\t{KeyNoPager, true},\n\t\t{KeyQuiet, true},\n\t\t{KeyNoInput, true},\n\t\t{KeyOutputFormat, false},\n\t\t{KeySelectedOrg, false},\n\t\t{KeyPager, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(string(tt.key), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif got := tt.key.IsBool(); got != tt.isBool {\n\t\t\t\tt.Errorf(\"%s.IsBool() = %v, want %v\", tt.key, got, tt.isBool)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConfigKeyIsUserOnly(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tkey        ConfigKey\n\t\tisUserOnly bool\n\t}{\n\t\t{KeyNoInput, true},\n\t\t{KeyPager, true},\n\t\t{KeyNoPager, false},\n\t\t{KeyQuiet, false},\n\t\t{KeyOutputFormat, false},\n\t\t{KeySelectedOrg, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(string(tt.key), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif got := tt.key.IsUserOnly(); got != tt.isUserOnly {\n\t\t\t\tt.Errorf(\"%s.IsUserOnly() = %v, want %v\", tt.key, got, tt.isUserOnly)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConfigKeyValidValues(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"output_format has valid values\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvalues := KeyOutputFormat.ValidValues()\n\t\tif values == nil {\n\t\t\tt.Fatal(\"expected valid values for output_format\")\n\t\t}\n\t\texpected := []string{\"json\", \"yaml\", \"text\"}\n\t\tif len(values) != len(expected) {\n\t\t\tt.Errorf(\"got %d values, want %d\", len(values), len(expected))\n\t\t}\n\t})\n\n\tt.Run(\"boolean keys have true/false\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfor _, key := range []ConfigKey{KeyNoPager, KeyQuiet, KeyNoInput} {\n\t\t\tvalues := key.ValidValues()\n\t\t\tif values == nil {\n\t\t\t\tt.Errorf(\"%s.ValidValues() = nil, want [true, false]\", key)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif len(values) != 2 || values[0] != \"true\" || values[1] != \"false\" {\n\t\t\t\tt.Errorf(\"%s.ValidValues() = %v, want [true, false]\", key, values)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"pager has no valid values constraint\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tif values := KeyPager.ValidValues(); values != nil {\n\t\t\tt.Errorf(\"KeyPager.ValidValues() = %v, want nil\", values)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "cmd/config/get.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\ntype GetCmd struct {\n\tKey string `arg:\"\" help:\"Configuration key to get\"`\n}\n\nfunc (c *GetCmd) Help() string {\n\treturn `Get a configuration value.\n\nReturns the effective value after applying precedence rules:\n  Environment variable > Local config (.bk.yaml) > User config (~/.config/bk.yaml) > Default\n\nValid keys:\n  selected_org   Organization slug to use\n  output_format  Default output format (json, yaml, text)\n  no_pager       Disable pager for text output (true, false)\n  quiet          Suppress progress output (true, false)\n  no_input       Disable interactive prompts (true, false)\n  pager          Custom pager command\n  experiments    Enabled experiment flags\n\nExamples:\n  $ bk config get output_format\n  $ bk config get pager`\n}\n\nfunc (c *GetCmd) Run() error {\n\tkey, err := ValidateKey(c.Key)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf, err := factory.New()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconf := f.Config\n\n\tvar value string\n\tswitch key {\n\tcase KeySelectedOrg:\n\t\tvalue = conf.OrganizationSlug()\n\tcase KeyOutputFormat:\n\t\tvalue = conf.OutputFormat()\n\tcase KeyNoPager:\n\t\tif conf.PagerDisabled() {\n\t\t\tvalue = \"true\"\n\t\t} else {\n\t\t\tvalue = \"false\"\n\t\t}\n\tcase KeyQuiet:\n\t\tif conf.Quiet() {\n\t\t\tvalue = \"true\"\n\t\t} else {\n\t\t\tvalue = \"false\"\n\t\t}\n\tcase KeyNoInput:\n\t\tif conf.NoInput() {\n\t\t\tvalue = \"true\"\n\t\t} else {\n\t\t\tvalue = \"false\"\n\t\t}\n\tcase KeyPager:\n\t\tvalue = conf.Pager()\n\tcase KeyExperiments:\n\t\tvalue = conf.Experiments()\n\t}\n\n\tif value != \"\" {\n\t\tfmt.Println(value)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/config/list.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\ntype ListCmd struct {\n\tLocal  bool `help:\"Only show local configuration\" xor:\"scope\"`\n\tGlobal bool `help:\"Only show global (user) configuration\" xor:\"scope\"`\n}\n\nfunc (c *ListCmd) Help() string {\n\treturn `List all configuration values.\n\nShows the effective configuration after applying precedence rules.\n\nExamples:\n  $ bk config list\n  $ bk config list --local\n  $ bk config list --global`\n}\n\nfunc (c *ListCmd) Run() error {\n\tf, err := factory.New()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconf := f.Config\n\tinGitRepo := f.GitRepository != nil\n\n\ttype configItem struct {\n\t\tkey    string\n\t\tvalue  string\n\t\tsource string\n\t}\n\n\tvar items []configItem\n\n\tif !c.Local {\n\t\t// Show global/user config values\n\t\tif v := conf.OrganizationSlug(); v != \"\" && !c.Global {\n\t\t\titems = append(items, configItem{string(KeySelectedOrg), v, \"effective\"})\n\t\t}\n\t\tif v := conf.OutputFormat(); v != \"\" {\n\t\t\titems = append(items, configItem{string(KeyOutputFormat), v, \"effective\"})\n\t\t}\n\t\tif conf.PagerDisabled() {\n\t\t\titems = append(items, configItem{string(KeyNoPager), \"true\", \"effective\"})\n\t\t}\n\t\tif conf.Quiet() {\n\t\t\titems = append(items, configItem{string(KeyQuiet), \"true\", \"effective\"})\n\t\t}\n\t\tif conf.NoInput() {\n\t\t\titems = append(items, configItem{string(KeyNoInput), \"true\", \"effective\"})\n\t\t}\n\t\tif v := conf.Pager(); v != \"\" && v != \"less -R\" {\n\t\t\titems = append(items, configItem{string(KeyPager), v, \"effective\"})\n\t\t}\n\t\tif v := conf.Experiments(); v != \"\" {\n\t\t\titems = append(items, configItem{string(KeyExperiments), v, \"effective\"})\n\t\t}\n\t}\n\n\tif c.Local && !inGitRepo {\n\t\tfmt.Fprintln(os.Stderr, \"warning: not in a git repository, no local config available\")\n\t\treturn nil\n\t}\n\n\tif len(items) == 0 {\n\t\tfmt.Println(\"No configuration values set.\")\n\t\treturn nil\n\t}\n\n\tfor _, item := range items {\n\t\tfmt.Printf(\"%s=%s\\n\", item.key, item.value)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/config/set.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\ntype SetCmd struct {\n\tKey   string `arg:\"\" help:\"Configuration key to set\"`\n\tValue string `arg:\"\" help:\"Value to set\"`\n\tLocal bool   `help:\"Save to local (.bk.yaml) instead of user config\"`\n}\n\nfunc (c *SetCmd) Help() string {\n\treturn `Set a configuration value.\n\nValid keys:\n  selected_org   Organization slug to use\n  output_format  Default output format (json, yaml, text)\n  no_pager       Disable pager for text output (true, false)\n  quiet          Suppress progress output (true, false)\n  no_input       Disable interactive prompts (true, false) [user config only]\n  pager          Custom pager command [user config only]\n  telemetry      Enable anonymous usage telemetry (true, false) [user config only]\n\nExamples:\n  # Set default output format to YAML\n  $ bk config set output_format yaml\n\n  # Disable pager globally\n  $ bk config set no_pager true\n\n  # Set repo-specific output format\n  $ bk config set output_format text --local\n\n  # Set a custom pager\n  $ bk config set pager \"less -RS\"`\n}\n\nfunc (c *SetCmd) Run() error {\n\tkey, err := ValidateKey(c.Key)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Validate the value\n\tif validValues := key.ValidValues(); validValues != nil {\n\t\tif !slices.Contains(validValues, c.Value) {\n\t\t\treturn fmt.Errorf(\"invalid value %q for %s\\nvalid values: %v\", c.Value, key, validValues)\n\t\t}\n\t}\n\n\t// Check if key can be set locally\n\tif c.Local && key.IsUserOnly() {\n\t\treturn fmt.Errorf(\"%s can only be set in user config (not --local)\", key)\n\t}\n\n\tf, err := factory.New()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn SetConfigValue(f.Config, key, c.Value, c.Local)\n}\n"
  },
  {
    "path": "cmd/config/unset.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\ntype UnsetCmd struct {\n\tKey   string `arg:\"\" help:\"Configuration key to unset\"`\n\tLocal bool   `help:\"Unset from local (.bk.yaml) instead of user config\"`\n}\n\nfunc (c *UnsetCmd) Help() string {\n\treturn `Remove a configuration value, reverting to default.\n\nExamples:\n  # Reset output format to default (json)\n  $ bk config unset output_format\n\n  # Remove repo-specific setting\n  $ bk config unset output_format --local\n\n  # Reset pager to default (less -R)\n  $ bk config unset pager`\n}\n\nfunc (c *UnsetCmd) Run() error {\n\tkey, err := ValidateKey(c.Key)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check if key can be unset locally\n\tif c.Local && key.IsUserOnly() {\n\t\treturn fmt.Errorf(\"%s can only be unset from user config (not --local)\", key)\n\t}\n\n\tf, err := factory.New()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn SetConfigValue(f.Config, key, \"\", c.Local)\n}\n"
  },
  {
    "path": "cmd/configure/configure.go",
    "content": "package configure\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n\tbkAuth \"github.com/buildkite/cli/v3/cmd/auth\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n)\n\ntype ConfigureCmd struct {\n\tOrg     string              `help:\"Organization slug\" optional:\"\"`\n\tToken   string              `help:\"API token\" optional:\"\"`\n\tForce   bool                `help:\"Force setting a new token\" optional:\"\"`\n\tDefault ConfigureDefaultCmd `cmd:\"\" optional:\"\" help:\"Configure Buildkite API token\" hidden:\"\" default:\"1\"`\n\tAdd     ConfigureAddCmd     `cmd:\"\" optional:\"\" help:\"Add configuration for a new organization\"`\n}\n\ntype ConfigureDefaultCmd struct{}\n\ntype ConfigureAddCmd struct{}\n\nfunc (c *ConfigureAddCmd) Help() string {\n\treturn `\nExamples:\n  # Interactively configure a new organization\n  $ bk configure add\n\n  # Configure a new organization non-interactively\n  $ bk configure add --org my-org --token my-token\n`\n}\n\nfunc (c *ConfigureCmd) Help() string {\n\treturn `\nExamples:\n  # Configure Buildkite API token\n  $ bk configure --org my-org --token my-token\n\n  # Force setting a new token\n  $ bk configure --force --org my-org --token my-token\n`\n}\n\nfunc (c *ConfigureCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tif kongCtx.Command() == \"configure default\" {\n\t\ttargetOrg := c.Org\n\t\tif targetOrg == \"\" {\n\t\t\ttargetOrg = f.Config.OrganizationSlug()\n\t\t}\n\t\tif !c.Force && targetOrg != \"\" && f.Config.APITokenForOrg(targetOrg) != \"\" {\n\t\t\treturn fmt.Errorf(\"API token already configured for organization %q. Use --force to overwrite\", targetOrg)\n\t\t}\n\t}\n\n\t// If flags are provided, use them directly\n\tif c.Org != \"\" && c.Token != \"\" {\n\t\treturn ConfigureWithCredentials(f, c.Org, c.Token)\n\t}\n\n\treturn ConfigureRun(f, c.Org)\n}\n\nfunc ConfigureWithCredentials(f *factory.Factory, org, token string) error {\n\treturn bkAuth.LoginWithToken(f, org, token)\n}\n\nfunc ConfigureRun(f *factory.Factory, org string) error {\n\tif org == \"\" {\n\t\t// Get organization slug\n\t\tinputOrg, err := promptForInput(\"Organization slug: \", false)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif inputOrg == \"\" {\n\t\t\treturn errors.New(\"organization slug cannot be empty\")\n\t\t}\n\t\torg = inputOrg\n\t}\n\t// Check if token already exists for this organization.\n\t// Use resolved token lookup so keychain-backed entries are detected.\n\texistingToken := getTokenForOrg(f, org)\n\tif existingToken != \"\" {\n\t\tfmt.Printf(\"Using existing API token for organization: %s\\n\", org)\n\t\treturn f.Config.SelectOrganization(org, f.GitRepository != nil)\n\t}\n\n\t// Get API token with password input (no echo)\n\ttoken, err := promptForInput(\"API Token: \", true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif token == \"\" {\n\t\treturn errors.New(\"API token cannot be empty\")\n\t}\n\n\tfmt.Println(\"API token set for organization:\", org)\n\treturn ConfigureWithCredentials(f, org, token)\n}\n\n// getTokenForOrg retrieves the resolved token for a specific organization.\nfunc getTokenForOrg(f *factory.Factory, org string) string {\n\treturn f.Config.APITokenForOrg(org)\n}\n\n// promptForInput handles terminal input with optional password masking\nfunc promptForInput(prompt string, isPassword bool) (string, error) {\n\tfmt.Print(prompt)\n\n\tif isPassword {\n\t\treturn io.ReadPassword()\n\t} else {\n\t\t// Use standard input for regular text\n\t\treader := bufio.NewReader(os.Stdin)\n\t\tinput, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\t// Trim whitespace and newlines\n\t\treturn strings.TrimSpace(input), nil\n\t}\n}\n"
  },
  {
    "path": "cmd/configure/configure_case_test.go",
    "content": "package configure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/keyring\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc TestConfigurePreservesOrganizationCase(t *testing.T) {\n\ttestCases := []struct {\n\t\tname        string\n\t\torgInput    string\n\t\texpectedOrg string\n\t}{\n\t\t{\n\t\t\tname:        \"preserves mixed case organization name\",\n\t\t\torgInput:    \"gridX\",\n\t\t\texpectedOrg: \"gridX\",\n\t\t},\n\t\t{\n\t\t\tname:        \"preserves uppercase organization name\",\n\t\t\torgInput:    \"ACME\",\n\t\t\texpectedOrg: \"ACME\",\n\t\t},\n\t\t{\n\t\t\tname:        \"preserves lowercase organization name\",\n\t\t\torgInput:    \"buildkite\",\n\t\t\texpectedOrg: \"buildkite\",\n\t\t},\n\t\t{\n\t\t\tname:        \"preserves camelCase organization name\",\n\t\t\torgInput:    \"myOrg\",\n\t\t\texpectedOrg: \"myOrg\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tkeyring.MockForTesting()\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := config.New(fs, nil)\n\t\t\tf := &factory.Factory{Config: conf}\n\n\t\t\ttoken := \"bk_test_token_12345\"\n\n\t\t\terr := ConfigureWithCredentials(f, tc.orgInput, token)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"ConfigureWithCredentials failed: %v\", err)\n\t\t\t}\n\n\t\t\tgotOrg := conf.OrganizationSlug()\n\t\t\tif gotOrg != tc.expectedOrg {\n\t\t\t\tt.Errorf(\"expected organization to be %q, got %q\", tc.expectedOrg, gotOrg)\n\t\t\t}\n\n\t\t\tkr := keyring.New()\n\t\t\tgotToken, _ := kr.Get(tc.orgInput)\n\t\t\tif gotToken != token {\n\t\t\t\tt.Errorf(\"expected token to be %q, got %q\", token, gotToken)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/configure/configure_test.go",
    "content": "package configure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/keyring\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc TestGetTokenForOrg(t *testing.T) {\n\tt.Run(\"returns empty string when no token exists\", func(t *testing.T) {\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\t\tf := &factory.Factory{Config: conf}\n\n\t\ttoken := getTokenForOrg(f, \"nonexistent\")\n\t\tif token != \"\" {\n\t\t\tt.Errorf(\"expected empty string, got %s\", token)\n\t\t}\n\t})\n\n\tt.Run(\"returns token when it exists in keychain\", func(t *testing.T) {\n\t\tkeyring.MockForTesting()\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\t\tf := &factory.Factory{Config: conf}\n\n\t\tkr := keyring.New()\n\t\tkr.Set(\"test-org\", \"bk_test_token_12345\")\n\n\t\ttoken := getTokenForOrg(f, \"test-org\")\n\t\tif token != \"bk_test_token_12345\" {\n\t\t\tt.Errorf(\"expected bk_test_token_12345, got %s\", token)\n\t\t}\n\t})\n\n\tt.Run(\"returns different tokens for different organizations\", func(t *testing.T) {\n\t\tkeyring.MockForTesting()\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\t\tf := &factory.Factory{Config: conf}\n\n\t\tkr := keyring.New()\n\t\tkr.Set(\"org1\", \"bk_test_token_org1\")\n\t\tkr.Set(\"org2\", \"bk_test_token_org2\")\n\n\t\tif getTokenForOrg(f, \"org1\") != \"bk_test_token_org1\" {\n\t\t\tt.Errorf(\"expected bk_test_token_org1 for org1\")\n\t\t}\n\t\tif getTokenForOrg(f, \"org2\") != \"bk_test_token_org2\" {\n\t\t\tt.Errorf(\"expected bk_test_token_org2 for org2\")\n\t\t}\n\t})\n}\n\nfunc TestConfigureWithCredentials(t *testing.T) {\n\tt.Run(\"configures organization and token\", func(t *testing.T) {\n\t\tkeyring.MockForTesting()\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\t\tf := &factory.Factory{Config: conf}\n\n\t\torg := \"test-org\"\n\t\ttoken := \"bk_test_token_12345\"\n\n\t\terr := ConfigureWithCredentials(f, org, token)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"expected no error, got %s\", err)\n\t\t}\n\n\t\tif conf.OrganizationSlug() != org {\n\t\t\tt.Errorf(\"expected organization to be %s, got %s\", org, conf.OrganizationSlug())\n\t\t}\n\n\t\tkr := keyring.New()\n\t\tgot, _ := kr.Get(org)\n\t\tif got != token {\n\t\t\tt.Errorf(\"expected token to be %s, got %s\", token, got)\n\t\t}\n\t})\n}\n\nfunc TestConfigureTokenReuse(t *testing.T) {\n\tt.Run(\"reuses existing token when available\", func(t *testing.T) {\n\t\tkeyring.MockForTesting()\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\t\tf := &factory.Factory{Config: conf}\n\n\t\torg := \"test-org\"\n\t\texistingToken := \"bk_existing_token_12345\"\n\n\t\t// Pre-configure a token in the keychain\n\t\tkr := keyring.New()\n\t\tkr.Set(org, existingToken)\n\n\t\t// Verify the token can be retrieved\n\t\tretrievedToken := getTokenForOrg(f, org)\n\t\tif retrievedToken != existingToken {\n\t\t\tt.Errorf(\"expected to retrieve existing token %s, got %s\", existingToken, retrievedToken)\n\t\t}\n\n\t\t// Configure with the existing token\n\t\terr := ConfigureWithCredentials(f, org, retrievedToken)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"expected no error, got %s\", err)\n\t\t}\n\n\t\tif conf.OrganizationSlug() != org {\n\t\t\tt.Errorf(\"expected organization to be %s, got %s\", org, conf.OrganizationSlug())\n\t\t}\n\n\t\tgot, _ := kr.Get(org)\n\t\tif got != existingToken {\n\t\t\tt.Errorf(\"expected token to be %s, got %s\", existingToken, got)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "cmd/generate/generate.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/Khan/genqlient/generate\"\n\t\"github.com/suessflorian/gqlfetch\"\n)\n\n//go:generate go run generate.go\nfunc main() {\n\tconst schemaFile = \"../../schema.graphql\"\n\n\tif _, err := os.Stat(schemaFile); errors.Is(err, os.ErrNotExist) {\n\t\theaders := http.Header{\n\t\t\t\"Authorization\": []string{fmt.Sprintf(\"Bearer %s\", os.Getenv(\"BUILDKITE_GRAPHQL_TOKEN\"))},\n\t\t}\n\n\t\tfmt.Printf(\"Generating new schema file at %s\\n\", schemaFile)\n\t\tschema, err := gqlfetch.BuildClientSchemaWithHeaders(context.Background(), \"https://graphql.buildkite.com/v1\", headers, false)\n\t\tif err != nil {\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tif err = os.WriteFile(schemaFile, []byte(schema), 0o644); err != nil {\n\t\t\tfmt.Println(err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tfmt.Printf(\"Schema written to %s\\n\", schemaFile)\n\t}\n\n\tfmt.Println(\"Generating GraphQL code\")\n\tgenerate.Main()\n}\n"
  },
  {
    "path": "cmd/init/init.go",
    "content": "package init\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/alecthomas/kong\"\n)\n\nconst (\n\tdefaultPipelineYAML = `steps:\n  - label: \"Hello, world! 👋\"\n    command: echo \"Hello, world!\"`\n)\n\ntype InitCmd struct{}\n\nfunc (c *InitCmd) Run(kongCtx *kong.Context) error {\n\tif found, path := findExistingPipelineFile(\"\"); found {\n\t\tfmt.Printf(\"✨ File found at %s. You're good to go!\\n\", path)\n\t\treturn nil\n\t}\n\n\tpipelineFile := filepath.Join(\".buildkite\", \"pipeline.yaml\")\n\terr := os.MkdirAll(filepath.Dir(pipelineFile), 0o755)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = os.WriteFile(pipelineFile, []byte(defaultPipelineYAML), 0o660)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Printf(\"✨ File created at %s. You're good to go!\\n\", pipelineFile)\n\n\treturn nil\n}\n\nfunc findExistingPipelineFile(base string) (bool, string) {\n\t// the order in which buildkite-agent checks for files\n\tpaths := []string{\n\t\t\"buildkite.yml\",\n\t\t\"buildkite.yaml\",\n\t\t\"buildkite.json\",\n\t\tfilepath.FromSlash(\".buildkite/pipeline.yml\"),\n\t\tfilepath.FromSlash(\".buildkite/pipeline.yaml\"),\n\t\tfilepath.FromSlash(\".buildkite/pipeline.json\"),\n\t\tfilepath.FromSlash(\"buildkite/pipeline.yml\"),\n\t\tfilepath.FromSlash(\"buildkite/pipeline.yaml\"),\n\t\tfilepath.FromSlash(\"buildkite/pipeline.json\"),\n\t}\n\n\tfor _, path := range paths {\n\t\tpath = filepath.Join(base, path)\n\t\tif _, err := os.Stat(path); err == nil {\n\t\t\treturn true, path\n\t\t}\n\t}\n\n\treturn false, \"\"\n}\n"
  },
  {
    "path": "cmd/init/init_test.go",
    "content": "package init\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestFindExistingPipelineFileWithNoFile(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"bk-cli-*\")\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tdefer os.RemoveAll(dir)\n\n\tif found, _ := findExistingPipelineFile(dir); found {\n\t\tt.Fail()\n\t}\n}\n\nfunc TestFindExistingPipelineFile(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"bk-cli-*\")\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tdefer os.RemoveAll(dir)\n\t_ = os.MkdirAll(filepath.Join(dir, \".buildkite\"), 0o755)\n\tf, _ := os.Create(filepath.Join(dir, \".buildkite\", \"pipeline.yml\"))\n\tdefer f.Close()\n\n\tif found, _ := findExistingPipelineFile(dir); !found {\n\t\tt.Fail()\n\t}\n}\n"
  },
  {
    "path": "cmd/job/cancel.go",
    "content": "package job\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/internal/graphql\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/internal/util\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n)\n\ntype CancelCmd struct {\n\tJobID string `arg:\"\" help:\"Job ID to cancel\" required:\"\"`\n\tWeb   bool   `help:\"Open the job in a web browser after it has been cancelled\" short:\"w\"`\n}\n\nfunc (c *CancelCmd) Help() string {\n\treturn `\nExamples:\n  # Cancel a job (with confirmation prompt)\n  $ bk job cancel 0190046e-e199-453b-a302-a21a4d649d31\n\n  # Cancel a job without confirmation (useful for automation)\n  $ bk job --yes cancel 0190046e-e199-453b-a302-a21a4d649d31\n\n  # Cancel a job and open it in browser\n  $ bk job --yes cancel 0190046e-e199-453b-a302-a21a4d649d31 --web\n`\n}\n\nfunc (c *CancelCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\tgraphqlID := util.GenerateGraphQLID(\"JobTypeCommand---\", c.JobID)\n\n\tconfirmed, err := bkIO.Confirm(f, fmt.Sprintf(\"Cancel job %s\", c.JobID))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !confirmed {\n\t\treturn nil\n\t}\n\n\treturn c.cancelJob(ctx, c.JobID, graphqlID, f)\n}\n\nfunc (c *CancelCmd) cancelJob(ctx context.Context, displayID, apiID string, f *factory.Factory) error {\n\tvar result *graphql.CancelJobResponse\n\tif err := bkIO.SpinWhile(f, fmt.Sprintf(\"Cancelling job %s\", displayID), func() error {\n\t\tvar apiErr error\n\t\tresult, apiErr = graphql.CancelJob(ctx, f.GraphQLClient, apiID)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tjob := result.JobTypeCommandCancel.JobTypeCommand\n\tfmt.Printf(\"Job canceled: %s\\n\", job.Url)\n\n\treturn util.OpenInWebBrowser(c.Web, job.Url)\n}\n"
  },
  {
    "path": "cmd/job/cancel_test.go",
    "content": "package job\n\nimport (\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/util\"\n)\n\nfunc TestCancelCmdStructure(t *testing.T) {\n\tt.Parallel()\n\n\tcmd := &CancelCmd{\n\t\tJobID: \"01993829-d2e7-4834-9611-bbeb8c1290db\",\n\t\tWeb:   true,\n\t}\n\n\tif cmd.JobID == \"\" {\n\t\tt.Error(\"JobID should be set\")\n\t}\n\n\tif !cmd.Web {\n\t\tt.Error(\"Web flag should be true\")\n\t}\n}\n\nfunc TestGraphQLIDGeneration(t *testing.T) {\n\tt.Parallel()\n\n\tjobUUID := \"01993829-d2e7-4834-9611-bbeb8c1290db\"\n\tgraphqlID := util.GenerateGraphQLID(\"JobTypeCommand---\", jobUUID)\n\n\tif graphqlID == \"\" {\n\t\tt.Error(\"GraphQL ID should not be empty\")\n\t}\n}\n"
  },
  {
    "path": "cmd/job/graphql/cancel.graphql",
    "content": "mutation CancelJob($jobId: ID!) {\n  jobTypeCommandCancel(input: { id: $jobId }) {\n    clientMutationId\n    jobTypeCommand {\n      id\n      uuid\n      state\n      url\n    }\n  }\n}\n"
  },
  {
    "path": "cmd/job/graphql/jobs.graphql",
    "content": "query FindClusters($org: ID!, $cursor: String) {\n  organization(slug: $org) {\n    clusters(first: 100, after: $cursor) {\n      edges {\n        node {\n          id\n          name\n        }\n      }\n      pageInfo {\n        hasNextPage\n        endCursor\n      }\n    }\n  }\n}\n\nquery FindQueuesForCluster($clusterId: ID!, $cursor: String) {\n  node(id: $clusterId) {\n    ... on Cluster {\n      id\n      name\n      queues(first: 100, after: $cursor) {\n        edges {\n          node {\n            id\n            key\n          }\n        }\n        pageInfo {\n          hasNextPage\n          endCursor\n        }\n      }\n    }\n  }\n}\n\nquery ListJobsByQueue($org: ID!, $clusterQueue: [ID!], $first: Int, $after: String) {\n  organization(slug: $org) {\n    jobs(clusterQueue: $clusterQueue, first: $first, after: $after) {\n      edges {\n        node {\n          ... on JobTypeCommand {\n            id\n            uuid\n            command\n            state\n            exitStatus\n            url\n            startedAt\n            finishedAt\n            createdAt\n            cluster {\n              id\n              name\n            }\n            clusterQueue {\n              id\n              key\n            }\n            agent {\n              id\n              name\n              hostname\n              metaData\n            }\n          }\n        }\n      }\n      pageInfo {\n        endCursor\n        hasNextPage\n      }\n    }\n  }\n}\n\nquery ListJobsByState($org: ID!, $state: [JobStates!], $first: Int, $after: String) {\n  organization(slug: $org) {\n    jobs(state: $state, first: $first, after: $after) {\n      edges {\n        node {\n          ... on JobTypeCommand {\n            id\n            uuid\n            label\n            command\n            state\n            exitStatus\n            url\n            startedAt\n            finishedAt\n            createdAt\n            cluster {\n              id\n              name\n            }\n            clusterQueue {\n              id\n              key\n            }\n            agent {\n              id\n              name\n              hostname\n              metaData\n            }\n          }\n        }\n      }\n      pageInfo {\n        endCursor\n        hasNextPage\n      }\n    }\n  }\n}\n\nquery ListJobsByAgentQueryRules($org: ID!, $agentQueryRules: [String!], $first: Int, $after: String) {\n  organization(slug: $org) {\n    jobs(first: $first, after: $after, agentQueryRules: $agentQueryRules) {\n      edges {\n        node {\n          ... on JobTypeCommand {\n            id\n            uuid\n            command\n            state\n            exitStatus\n            url\n            startedAt\n            finishedAt\n            createdAt \n            agent {\n              id\n              name\n              hostname\n              metaData\n            }\n          }\n        }\n      }\n      pageInfo {\n        endCursor\n        hasNextPage\n      }\n    }\n  }\n}"
  },
  {
    "path": "cmd/job/graphql/retry.graphql",
    "content": "mutation RetryJob($id: ID!) {\n    jobTypeCommandRetry(input: {id: $id}) {\n        jobTypeCommand {\n            id\n            state\n            url\n        }\n    }\n}\n"
  },
  {
    "path": "cmd/job/graphql/unblock.graphql",
    "content": "mutation UnblockJob($id: ID!, $fields: JSON) {\n    jobTypeBlockUnblock(input: {id: $id, fields: $fields}) {\n        jobTypeBlock {\n            id\n            state\n            isUnblockable\n            build {\n                url\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cmd/job/list.go",
    "content": "package job\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/Khan/genqlient/graphql\"\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkGraphQL \"github.com/buildkite/cli/v3/internal/graphql\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nconst (\n\tmaxJobLimit = 5000\n\tpageSize    = 100\n)\n\ntype ListCmd struct {\n\tPipeline string   `help:\"Filter by pipeline slug\" short:\"p\"`\n\tSince    string   `help:\"Filter jobs from builds created since this time (e.g. 1h, 30m)\"`\n\tUntil    string   `help:\"Filter jobs from builds created before this time (e.g. 1h, 30m)\"`\n\tDuration string   `help:\"Filter by duration (e.g. >10m, <5m, 20m) - supports >, <, >=, <= operators\"`\n\tState    []string `help:\"Filter by job state\"`\n\tQueue    string   `help:\"Filter by queue name\"`\n\tOrderBy  string   `help:\"Order results by field (start_time, duration)\" name:\"order-by\"`\n\tLimit    int      `help:\"Maximum number of jobs to return\" default:\"100\"`\n\tNoLimit  bool     `help:\"Fetch all jobs (overrides --limit)\" name:\"no-limit\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ListCmd) Help() string {\n\treturn `This command supports both server-side filtering (fast) and client-side filtering.\nServer-side filters are applied when fetching builds, while client-side filters\nare applied after extracting jobs from builds.\n\nClient-side filters: --queue, --state, --duration\nServer-side filters: --pipeline, --since, --until\n\nBy default, fetches up to 200 builds for filtering. Use --no-limit if you need to\nsearch across more builds to find all matching jobs.\n\nJobs can be filtered by queue, state, duration, and other attributes.\nWhen filtering by duration, you can use operators like >, <, >=, and <= to specify your criteria.\nSupported duration units are seconds (s), minutes (m), and hours (h).\n\nExamples:\n  # List recent jobs (100 by default)\n  $ bk job list\n\n  # List jobs from a specific queue\n  $ bk job list --queue test-queue\n\n  # List running jobs\n  $ bk job list --state running\n\n  # List jobs that took longer than 10 minutes\n  $ bk job list --duration \">10m\"\n\n  # List jobs from the last hour\n  $ bk job list --since 1h\n\n  # Combine filters\n  $ bk job list --queue test-queue --state running --duration \">10m\"\n\n  # Fetch all jobs matching filters (no limit)\n  $ bk job list --duration \">10m\" --no-limit\n\n  # Order by duration (longest first)\n  $ bk job list --order-by duration\n\n  # Get JSON output for bulk operations\n  $ bk job list --queue test-queue -o json\n`\n}\n\ntype jobListOptions struct {\n\tpipeline string\n\tsince    string\n\tuntil    string\n\tduration string\n\tstate    []string\n\tqueue    string\n\torderBy  string\n\tlimit    int\n\tnoLimit  bool\n}\n\nfunc (opts jobListOptions) withoutQueue() jobListOptions {\n\tnewOpts := opts\n\tnewOpts.queue = \"\"\n\treturn newOpts\n}\n\nfunc (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tif !c.NoLimit && c.Limit > maxJobLimit {\n\t\treturn fmt.Errorf(\"limit cannot exceed %d jobs (requested: %d); if you need more, use --no-limit\", maxJobLimit, c.Limit)\n\t}\n\n\topts := jobListOptions{\n\t\tpipeline: c.Pipeline,\n\t\tsince:    c.Since,\n\t\tuntil:    c.Until,\n\t\tduration: c.Duration,\n\t\tstate:    c.State,\n\t\tqueue:    c.Queue,\n\t\torderBy:  c.OrderBy,\n\t\tlimit:    c.Limit,\n\t\tnoLimit:  c.NoLimit,\n\t}\n\n\tlistOpts, err := jobListOptionsFromFlags(&opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\torg := f.Config.OrganizationSlug()\n\tvar jobs []buildkite.Job\n\n\tif err = bkIO.SpinWhile(f, \"Loading jobs\", func() error {\n\t\tif opts.queue != \"\" {\n\t\t\tjobs, err = fetchJobsWithQueueFilter(ctx, f, org, opts)\n\t\t} else {\n\t\t\tjobs, err = fetchJobs(ctx, f, org, opts, listOpts)\n\t\t}\n\t\treturn err\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"failed to list jobs: %w\", err)\n\t}\n\n\tif opts.queue == \"\" && (len(opts.state) > 0 || opts.duration != \"\") {\n\t\tjobs, err = applyClientSideFilters(jobs, opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to apply filters: %w\", err)\n\t\t}\n\t}\n\n\tif opts.orderBy != \"\" {\n\t\tjobs = sortJobs(jobs, opts.orderBy)\n\t}\n\n\t// Apply limit only if --no-limit is not set\n\tif !opts.noLimit && len(jobs) > opts.limit {\n\t\tjobs = jobs[:opts.limit]\n\t}\n\n\tif len(jobs) == 0 {\n\t\tif format != output.FormatText {\n\t\t\treturn output.Write(os.Stdout, []buildkite.Job{}, format)\n\t\t}\n\t\tfmt.Println(\"No jobs found matching the specified criteria.\")\n\t\treturn nil\n\t}\n\n\tif format == output.FormatText {\n\t\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\t\tdefer func() { _ = cleanup() }()\n\n\t\ttarget := org\n\t\tif c.Pipeline != \"\" {\n\t\t\ttarget = fmt.Sprintf(\"%s/%s\", org, c.Pipeline)\n\t\t}\n\n\t\tfmt.Fprintf(writer, \"Showing %d jobs for %s\\n\\n\", len(jobs), target)\n\t\treturn displayJobs(jobs, format, writer)\n\t}\n\n\treturn displayJobs(jobs, format, os.Stdout)\n}\n\nfunc fetchJobs(ctx context.Context, f *factory.Factory, org string, opts jobListOptions, listOpts *buildkite.BuildsListOptions) ([]buildkite.Job, error) {\n\tvar maxBuildsToFetch int\n\tif opts.noLimit {\n\t\t// When --no-limit is set, fetch all available builds (no upper bound)\n\t\tmaxBuildsToFetch = 0 // 0 means unlimited\n\t} else {\n\t\t// By default, fetch a reasonable number of builds (200 = 2 pages)\n\t\t// This provides a good pool for filtering without being tied to --limit\n\t\tmaxBuildsToFetch = 200\n\t}\n\n\tallJobs := make([]buildkite.Job, 0, opts.limit*2)\n\tbuildsFetched := 0\n\n\t// Calculate max pages (0 means unlimited)\n\tvar maxPages int\n\tif maxBuildsToFetch > 0 {\n\t\tmaxPages = (maxBuildsToFetch + pageSize - 1) / pageSize\n\t}\n\n\tfor page := 1; ; page++ {\n\t\t// Check page limit if set\n\t\tif maxPages > 0 && page > maxPages {\n\t\t\tbreak\n\t\t}\n\t\tlistOpts.Page = page\n\t\tlistOpts.PerPage = pageSize\n\n\t\tvar builds []buildkite.Build\n\t\tvar err error\n\n\t\tif opts.pipeline != \"\" {\n\t\t\tbuilds, err = getBuildsByPipeline(ctx, f, org, opts.pipeline, listOpts)\n\t\t} else {\n\t\t\tbuilds, _, err = f.RestAPIClient.Builds.ListByOrg(ctx, org, listOpts)\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif len(builds) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tbuildsFetched += len(builds)\n\n\t\tfor _, build := range builds {\n\t\t\tif len(allJobs)+len(build.Jobs) > cap(allJobs) {\n\t\t\t\tnewJobs := make([]buildkite.Job, len(allJobs), len(allJobs)+len(build.Jobs)+100)\n\t\t\t\tcopy(newJobs, allJobs)\n\t\t\t\tallJobs = newJobs\n\t\t\t}\n\t\t\tallJobs = append(allJobs, build.Jobs...)\n\t\t}\n\n\t\t// Stop if we got fewer builds than requested (last page)\n\t\tif len(builds) < pageSize {\n\t\t\tbreak\n\t\t}\n\n\t\t// Stop if we've reached the maximum builds to fetch (only when limit is set)\n\t\tif maxBuildsToFetch > 0 && buildsFetched >= maxBuildsToFetch {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn allJobs, nil\n}\n\ntype listJobsByQueue func(ctx context.Context, f *factory.Factory, org string, queueIDs []string, cursor *string) ([]buildkite.Job, *string, bool, error)\n\nfunc listJobsWithPagination(ctx context.Context, f *factory.Factory, org string, queueIDs []string, opts jobListOptions, listJobs listJobsByQueue) ([]buildkite.Job, error) {\n\tvar jobs []buildkite.Job\n\tvar cursor *string\n\tnoQueueOpts := opts.withoutQueue()\n\n\tfor len(jobs) < opts.limit {\n\t\tjobBatch, nextCursor, hasNext, err := listJobs(ctx, f, org, queueIDs, cursor)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(jobBatch) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// Apply client-side filters if needed\n\t\tif len(noQueueOpts.state) > 0 || noQueueOpts.duration != \"\" {\n\t\t\tjobBatch, err = applyClientSideFilters(jobBatch, noQueueOpts)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to apply filters: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tfor _, job := range jobBatch {\n\t\t\tif len(jobs) >= opts.limit {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tjobs = append(jobs, job)\n\t\t}\n\n\t\tif !hasNext {\n\t\t\tbreak\n\t\t}\n\t\tcursor = nextCursor\n\t}\n\n\treturn jobs, nil\n}\n\nfunc fetchJobsWithQueueFilter(ctx context.Context, f *factory.Factory, org string, opts jobListOptions) ([]buildkite.Job, error) {\n\tqueueIDs, err := lookupQueueIDs(ctx, f, org, opts.queue)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(queueIDs) == 0 {\n\t\t// Fallback to unclustered agent query rules\n\t\tagentQueryRules := []string{\"queue=\" + strings.ToLower(opts.queue)}\n\t\treturn listJobsWithPagination(ctx, f, org, agentQueryRules, opts, listJobsByAgentQueryRules)\n\t}\n\n\treturn listJobsWithPagination(ctx, f, org, queueIDs, opts, listJobsByClusterQueue)\n}\n\nconst maxConcurrentRequests = 10 // Balance between performance and API rate limits\n\ntype ClusterInfo struct {\n\tID   string\n\tName string\n}\n\nfunc lookupQueueIDs(ctx context.Context, f *factory.Factory, org, queueName string) ([]string, error) {\n\tclusters, err := fetchAllClusters(ctx, f.GraphQLClient, org)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to fetch clusters: %w\", err)\n\t}\n\n\tif len(clusters) == 0 {\n\t\treturn []string{}, nil\n\t}\n\n\treturn fetchQueuesFromClusters(ctx, f.GraphQLClient, clusters, queueName)\n}\n\nfunc fetchAllClusters(ctx context.Context, client graphql.Client, org string) ([]ClusterInfo, error) {\n\tvar allClusters []ClusterInfo\n\tvar cursor *string\n\n\tfor {\n\t\tresp, err := bkGraphQL.FindClusters(ctx, client, org, cursor)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif resp.Organization == nil || resp.Organization.Clusters == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, edge := range resp.Organization.Clusters.Edges {\n\t\t\tif edge.Node != nil {\n\t\t\t\tallClusters = append(allClusters, ClusterInfo{\n\t\t\t\t\tID:   edge.Node.Id,\n\t\t\t\t\tName: edge.Node.Name,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tif resp.Organization.Clusters.PageInfo != nil && resp.Organization.Clusters.PageInfo.HasNextPage {\n\t\t\tcursor = resp.Organization.Clusters.PageInfo.EndCursor\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn allClusters, nil\n}\n\nfunc fetchQueuesFromClusters(ctx context.Context, client graphql.Client, clusters []ClusterInfo, queueName string) ([]string, error) {\n\tresultChan := make(chan []string, len(clusters))\n\terrorChan := make(chan error, len(clusters))\n\tsemaphore := make(chan struct{}, maxConcurrentRequests)\n\n\tvar wg sync.WaitGroup\n\n\tfor _, cluster := range clusters {\n\t\twg.Add(1)\n\t\tgo func(c ClusterInfo) {\n\t\t\tdefer wg.Done()\n\n\t\t\tsemaphore <- struct{}{}\n\t\t\tdefer func() { <-semaphore }()\n\t\t\tqueueIDs, err := fetchQueuesForCluster(ctx, client, c.ID, queueName)\n\t\t\tif err != nil {\n\t\t\t\terrorChan <- fmt.Errorf(\"cluster %s: %w\", c.Name, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresultChan <- queueIDs\n\t\t}(cluster)\n\t}\n\n\tvar allQueueIDs []string\n\tvar results int\n\texpectedResults := len(clusters)\n\n\tfor results < expectedResults {\n\t\tselect {\n\t\tcase queueIDs := <-resultChan:\n\t\t\tallQueueIDs = append(allQueueIDs, queueIDs...)\n\t\t\tresults++\n\n\t\tcase err := <-errorChan:\n\t\t\treturn nil, err\n\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\t}\n\t}\n\n\treturn allQueueIDs, nil\n}\n\nfunc fetchQueuesForCluster(ctx context.Context, client graphql.Client, clusterID, queueName string) ([]string, error) {\n\tvar matchingQueueIDs []string\n\tvar cursor *string\n\ttargetLower := strings.ToLower(queueName)\n\n\tfor {\n\t\tresp, err := bkGraphQL.FindQueuesForCluster(ctx, client, clusterID, cursor)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif resp.Node == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tcluster, ok := (*resp.Node).(*bkGraphQL.FindQueuesForClusterNodeCluster)\n\t\tif !ok || cluster == nil || cluster.Queues == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, edge := range cluster.Queues.Edges {\n\t\t\tif edge.Node != nil && strings.ToLower(edge.Node.Key) == targetLower {\n\t\t\t\tmatchingQueueIDs = append(matchingQueueIDs, edge.Node.Id)\n\t\t\t}\n\t\t}\n\n\t\tif cluster.Queues.PageInfo != nil && cluster.Queues.PageInfo.HasNextPage {\n\t\t\tcursor = cluster.Queues.PageInfo.EndCursor\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn matchingQueueIDs, nil\n}\n\nfunc listJobsByClusterQueue(ctx context.Context, f *factory.Factory, org string, queueIDs []string, cursor *string) ([]buildkite.Job, *string, bool, error) {\n\tfirst := pageSize\n\tresp, err := bkGraphQL.ListJobsByQueue(ctx, f.GraphQLClient, org, queueIDs, &first, cursor)\n\tif err != nil {\n\t\treturn nil, nil, false, fmt.Errorf(\"failed to list jobs: %w\", err)\n\t}\n\n\tif resp.Organization == nil || resp.Organization.Jobs == nil {\n\t\treturn []buildkite.Job{}, nil, false, nil\n\t}\n\n\tvar jobs []buildkite.Job\n\tfor _, edge := range resp.Organization.Jobs.Edges {\n\t\tif edge.Node != nil {\n\t\t\tjobs = append(jobs, convertGraphQLJobToBuildkiteJob(edge.Node))\n\t\t}\n\t}\n\n\thasMore := resp.Organization.Jobs.PageInfo != nil && resp.Organization.Jobs.PageInfo.HasNextPage\n\tnextCursor := (*string)(nil)\n\tif hasMore && resp.Organization.Jobs.PageInfo.EndCursor != nil {\n\t\tnextCursor = resp.Organization.Jobs.PageInfo.EndCursor\n\t}\n\n\treturn jobs, nextCursor, hasMore, nil\n}\n\nfunc convertGraphQLJobToBuildkiteJob(jobNode *bkGraphQL.ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob) buildkite.Job {\n\t// Handle the union type - we only care about JobTypeCommand for now\n\tswitch job := (*jobNode).(type) {\n\tcase *bkGraphQL.ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand:\n\t\tstartedAt := convertTimestamp(job.StartedAt)\n\t\tfinishedAt := convertTimestamp(job.FinishedAt)\n\t\tcreatedAt := convertTimestamp(job.CreatedAt)\n\t\tagent := convertAgent(job.Agent)\n\n\t\t// Build label (jobs don't have labels in GraphQL, so we use command or empty)\n\t\tlabel := derefString(job.Command)\n\n\t\treturn buildkite.Job{\n\t\t\tID:              job.Id,\n\t\t\tType:            \"script\",\n\t\t\tName:            job.Uuid, // Use UUID as name\n\t\t\tLabel:           label,\n\t\t\tCommand:         derefString(job.Command),\n\t\t\tState:           mapGraphQLState(string(job.State), derefString(job.ExitStatus)),\n\t\t\tWebURL:          job.Url,\n\t\t\tStartedAt:       startedAt,\n\t\t\tFinishedAt:      finishedAt,\n\t\t\tCreatedAt:       createdAt,\n\t\t\tAgent:           agent,\n\t\t\tAgentQueryRules: []string{}, // Empty for GraphQL jobs\n\t\t}\n\tdefault:\n\t\t// For non-command jobs, return a minimal job struct\n\t\treturn buildkite.Job{\n\t\t\tID:    \"unknown\",\n\t\t\tType:  \"unknown\",\n\t\t\tState: \"unknown\",\n\t\t}\n\t}\n}\n\nfunc listJobsByAgentQueryRules(ctx context.Context, f *factory.Factory, org string, agentQueryRules []string, cursor *string) ([]buildkite.Job, *string, bool, error) {\n\tfirst := pageSize\n\n\tresp, err := bkGraphQL.ListJobsByAgentQueryRules(ctx, f.GraphQLClient, org, agentQueryRules, &first, cursor)\n\tif err != nil {\n\t\treturn nil, nil, false, fmt.Errorf(\"failed to list jobs: %w\", err)\n\t}\n\n\tif resp.Organization == nil || resp.Organization.Jobs == nil {\n\t\treturn []buildkite.Job{}, nil, false, nil\n\t}\n\n\tvar jobs []buildkite.Job\n\tfor _, edge := range resp.Organization.Jobs.Edges {\n\t\tif edge.Node != nil {\n\t\t\tjobs = append(jobs, convertGraphQLAgentQueryRulesJobToBuildkiteJob(edge.Node, agentQueryRules))\n\t\t}\n\t}\n\n\thasMore := resp.Organization.Jobs.PageInfo != nil && resp.Organization.Jobs.PageInfo.HasNextPage\n\tnextCursor := (*string)(nil)\n\tif hasMore && resp.Organization.Jobs.PageInfo.EndCursor != nil {\n\t\tnextCursor = resp.Organization.Jobs.PageInfo.EndCursor\n\t}\n\treturn jobs, nextCursor, hasMore, nil\n}\n\nfunc convertGraphQLAgentQueryRulesJobToBuildkiteJob(jobNode *bkGraphQL.ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob, agentQueryRules []string) buildkite.Job {\n\t// Handle the union type - we only care about JobTypeCommand for now\n\tvar agent buildkite.Agent\n\tswitch job := (*jobNode).(type) {\n\tcase *bkGraphQL.ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand:\n\t\tstartedAt := convertTimestamp(job.StartedAt)\n\t\tfinishedAt := convertTimestamp(job.FinishedAt)\n\t\tcreatedAt := convertTimestamp(job.CreatedAt)\n\n\t\tif job.Agent != nil {\n\t\t\tagent = buildkite.Agent{\n\t\t\t\tID:       job.Agent.Id,\n\t\t\t\tName:     job.Agent.Name,\n\t\t\t\tHostname: derefString(job.Agent.Hostname),\n\t\t\t\tMetadata: job.Agent.MetaData,\n\t\t\t}\n\t\t}\n\n\t\t// Build label (jobs don't have labels in GraphQL, so we use command or empty)\n\t\tlabel := derefString(job.Command)\n\n\t\treturn buildkite.Job{\n\t\t\tID:              job.Id,\n\t\t\tType:            \"script\",\n\t\t\tName:            job.Uuid, // Use UUID as name\n\t\t\tLabel:           label,\n\t\t\tCommand:         derefString(job.Command),\n\t\t\tState:           mapGraphQLState(string(job.State), derefString(job.ExitStatus)),\n\t\t\tWebURL:          job.Url,\n\t\t\tStartedAt:       startedAt,\n\t\t\tFinishedAt:      finishedAt,\n\t\t\tCreatedAt:       createdAt,\n\t\t\tAgent:           agent,\n\t\t\tAgentQueryRules: agentQueryRules,\n\t\t}\n\tdefault:\n\t\t// For non-command jobs, return a minimal job struct\n\t\treturn buildkite.Job{\n\t\t\tID:    \"unknown\",\n\t\t\tType:  \"unknown\",\n\t\t\tState: \"unknown\",\n\t\t}\n\t}\n}\n\nfunc convertTimestamp(t *time.Time) *buildkite.Timestamp {\n\tif t == nil {\n\t\treturn nil\n\t}\n\treturn &buildkite.Timestamp{Time: *t}\n}\n\nfunc convertAgent(agentNode *bkGraphQL.ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) buildkite.Agent {\n\tif agentNode == nil {\n\t\treturn buildkite.Agent{}\n\t}\n\n\treturn buildkite.Agent{\n\t\tID:       agentNode.Id,\n\t\tName:     agentNode.Name,\n\t\tHostname: derefString(agentNode.Hostname),\n\t\tMetadata: agentNode.MetaData,\n\t}\n}\n\nfunc derefString(s *string) string {\n\tif s == nil {\n\t\treturn \"\"\n\t}\n\treturn *s\n}\n\n// mapGraphQLState converts GraphQL job states to REST API equivalent states\nfunc mapGraphQLState(graphqlState, exitStatus string) string {\n\tswitch graphqlState {\n\tcase \"FINISHED\":\n\t\t// For finished jobs, determine success/failure based on exit status\n\t\tif exitStatus == \"0\" {\n\t\t\treturn \"passed\"\n\t\t}\n\t\treturn \"failed\"\n\tcase \"RUNNING\":\n\t\treturn \"running\"\n\tcase \"SCHEDULED\", \"ASSIGNED\", \"ACCEPTED\":\n\t\treturn \"scheduled\"\n\tcase \"CANCELED\", \"CANCELING\":\n\t\treturn \"canceled\"\n\tcase \"TIMED_OUT\", \"TIMING_OUT\":\n\t\treturn \"timed_out\"\n\tcase \"SKIPPED\":\n\t\treturn \"skipped\"\n\tcase \"BLOCKED\":\n\t\treturn \"blocked\"\n\tcase \"WAITING\":\n\t\treturn \"waiting\"\n\tdefault:\n\t\t// For unknown states, return lowercase version of GraphQL state\n\t\treturn strings.ToLower(graphqlState)\n\t}\n}\n\nfunc jobListOptionsFromFlags(opts *jobListOptions) (*buildkite.BuildsListOptions, error) {\n\tlistOpts := &buildkite.BuildsListOptions{\n\t\tListOptions: buildkite.ListOptions{\n\t\t\tPerPage: pageSize,\n\t\t},\n\t}\n\n\tnow := time.Now()\n\tif opts.since != \"\" {\n\t\td, err := time.ParseDuration(opts.since)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid since duration '%s': %w\", opts.since, err)\n\t\t}\n\t\tlistOpts.CreatedFrom = now.Add(-d)\n\t}\n\n\tif opts.until != \"\" {\n\t\td, err := time.ParseDuration(opts.until)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid until duration '%s': %w\", opts.until, err)\n\t\t}\n\t\tlistOpts.CreatedTo = now.Add(-d)\n\t}\n\n\treturn listOpts, nil\n}\n\nfunc getBuildsByPipeline(ctx context.Context, f *factory.Factory, org, pipelineFlag string, listOpts *buildkite.BuildsListOptions) ([]buildkite.Build, error) {\n\tpipelineRes := pipelineResolver.NewAggregateResolver(\n\t\tpipelineResolver.ResolveFromFlag(pipelineFlag, f.Config),\n\t\tpipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)),\n\t)\n\n\tpipeline, err := pipelineRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbuilds, _, err := f.RestAPIClient.Builds.ListByPipeline(ctx, org, pipeline.Name, listOpts)\n\treturn builds, err\n}\n\nfunc applyClientSideFilters(jobs []buildkite.Job, opts jobListOptions) ([]buildkite.Job, error) {\n\tif opts.queue == \"\" && len(opts.state) == 0 && opts.duration == \"\" {\n\t\treturn jobs, nil\n\t}\n\n\tvar durationOp string\n\tvar durationThreshold time.Duration\n\tvar normalizedStates []string\n\n\tif len(opts.state) > 0 {\n\t\tnormalizedStates = make([]string, len(opts.state))\n\t\tfor i, state := range opts.state {\n\t\t\tnormalizedStates[i] = strings.ToLower(state)\n\t\t}\n\t}\n\n\tif opts.duration != \"\" {\n\t\tdurationOp = \">=\"\n\t\tdurationStr := opts.duration\n\n\t\tswitch {\n\t\tcase strings.HasPrefix(opts.duration, \"<\"):\n\t\t\tdurationOp = \"<\"\n\t\t\tdurationStr = opts.duration[1:]\n\t\tcase strings.HasPrefix(opts.duration, \">\"):\n\t\t\tdurationOp = \">\"\n\t\t\tdurationStr = opts.duration[1:]\n\t\t}\n\n\t\td, err := time.ParseDuration(durationStr)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid duration format: %w\", err)\n\t\t}\n\t\tdurationThreshold = d\n\t}\n\n\tresult := make([]buildkite.Job, 0, len(jobs)/2)\n\n\tfor i := range jobs {\n\t\tjob := &jobs[i]\n\n\t\tif opts.queue != \"\" {\n\t\t\tif !matchesQueue(*job, opts.queue) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif len(normalizedStates) > 0 {\n\t\t\tif !containsString(normalizedStates, strings.ToLower(job.State)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif opts.duration != \"\" {\n\t\t\tif job.StartedAt == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar elapsed time.Duration\n\t\t\tif job.FinishedAt != nil {\n\t\t\t\telapsed = job.FinishedAt.Sub(job.StartedAt.Time)\n\t\t\t} else {\n\t\t\t\telapsed = time.Since(job.StartedAt.Time)\n\t\t\t}\n\n\t\t\tswitch durationOp {\n\t\t\tcase \"<\":\n\t\t\t\tif elapsed >= durationThreshold {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase \">\":\n\t\t\t\tif elapsed <= durationThreshold {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tif elapsed < durationThreshold {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresult = append(result, *job)\n\t}\n\n\treturn result, nil\n}\n\nfunc matchesQueue(job buildkite.Job, queueFilter string) bool {\n\tfor _, rule := range job.AgentQueryRules {\n\t\tif strings.Contains(strings.ToLower(rule), \"queue=\"+strings.ToLower(queueFilter)) {\n\t\t\treturn true\n\t\t}\n\t\tif strings.EqualFold(rule, queueFilter) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tfor _, meta := range job.Agent.Metadata {\n\t\tif strings.Contains(strings.ToLower(meta), \"queue=\"+strings.ToLower(queueFilter)) {\n\t\t\treturn true\n\t\t}\n\t\tif strings.EqualFold(meta, queueFilter) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc sortJobs(jobs []buildkite.Job, orderBy string) []buildkite.Job {\n\tif orderBy == \"\" {\n\t\treturn jobs\n\t}\n\n\tsort.Slice(jobs, func(i, j int) bool {\n\t\tswitch orderBy {\n\t\tcase \"start_time\":\n\t\t\tif jobs[i].StartedAt == nil && jobs[j].StartedAt == nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif jobs[i].StartedAt == nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif jobs[j].StartedAt == nil {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn jobs[i].StartedAt.Before(jobs[j].StartedAt.Time)\n\t\tcase \"duration\":\n\t\t\tdurI := getJobDuration(jobs[i])\n\t\t\tdurJ := getJobDuration(jobs[j])\n\t\t\treturn durI > durJ\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t})\n\n\treturn jobs\n}\n\nfunc getJobDuration(job buildkite.Job) time.Duration {\n\tif job.StartedAt == nil {\n\t\treturn 0\n\t}\n\tif job.FinishedAt != nil {\n\t\treturn job.FinishedAt.Sub(job.StartedAt.Time)\n\t}\n\treturn time.Since(job.StartedAt.Time)\n}\n\nfunc displayJobs(jobs []buildkite.Job, format output.Format, writer io.Writer) error {\n\tif format != output.FormatText {\n\t\treturn output.Write(writer, jobs, format)\n\t}\n\n\tconst (\n\t\tmaxLabelLength  = 35\n\t\ttruncatedLength = 32\n\t\ttimeFormat      = \"2006-01-02T15:04:05Z\"\n\t)\n\n\theaders := []string{\"State\", \"Label\", \"Started (UTC)\", \"Finished (UTC)\", \"Duration\", \"URL\"}\n\tvar rows [][]string\n\n\tfor _, job := range jobs {\n\t\tlabel := job.Label\n\t\tif label == \"\" {\n\t\t\tlabel = job.Name\n\t\t}\n\t\tif len(label) > maxLabelLength {\n\t\t\tlabel = label[:truncatedLength] + \"...\"\n\t\t}\n\n\t\tstartedAt := \"-\"\n\t\tif job.StartedAt != nil {\n\t\t\tstartedAt = job.StartedAt.Format(timeFormat)\n\t\t}\n\n\t\tfinishedAt := \"-\"\n\t\tduration := \"-\"\n\t\tif job.FinishedAt != nil {\n\t\t\tfinishedAt = job.FinishedAt.Format(timeFormat)\n\t\t\tif job.StartedAt != nil {\n\t\t\t\tdur := job.FinishedAt.Sub(job.StartedAt.Time)\n\t\t\t\tduration = formatDuration(dur)\n\t\t\t}\n\t\t} else if job.StartedAt != nil {\n\t\t\tdur := time.Since(job.StartedAt.Time)\n\t\t\tduration = formatDuration(dur) + \" (running)\"\n\t\t}\n\n\t\trows = append(rows, []string{\n\t\t\tjob.State,\n\t\t\tlabel,\n\t\t\tstartedAt,\n\t\t\tfinishedAt,\n\t\t\tduration,\n\t\t\tjob.WebURL,\n\t\t})\n\t}\n\n\ttable := output.Table(headers, rows, map[string]string{\n\t\t\"state\":          \"bold\",\n\t\t\"label\":          \"italic\",\n\t\t\"started (utc)\":  \"dim\",\n\t\t\"finished (utc)\": \"dim\",\n\t\t\"duration\":       \"bold\",\n\t\t\"url\":            \"dim\",\n\t})\n\n\tfmt.Fprint(writer, table)\n\treturn nil\n}\n\nfunc formatDuration(d time.Duration) string {\n\tif d < time.Minute {\n\t\treturn fmt.Sprintf(\"%.0fs\", d.Seconds())\n\t}\n\tif d < time.Hour {\n\t\tminutes := d / time.Minute\n\t\tseconds := (d % time.Minute) / time.Second\n\t\treturn fmt.Sprintf(\"%dm%ds\", minutes, seconds)\n\t}\n\thours := d / time.Hour\n\tminutes := (d % time.Hour) / time.Minute\n\treturn fmt.Sprintf(\"%dh%dm\", hours, minutes)\n}\n\nfunc containsString(slice []string, item string) bool {\n\tfor _, s := range slice {\n\t\tif strings.EqualFold(s, item) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "cmd/job/list_test.go",
    "content": "package job\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestDisplayJobs_EmptyJSON(t *testing.T) {\n\tvar buf bytes.Buffer\n\terr := displayJobs([]buildkite.Job{}, output.FormatJSON, &buf)\n\tif err != nil {\n\t\tt.Fatalf(\"displayJobs failed: %v\", err)\n\t}\n\n\tgot := strings.TrimSpace(buf.String())\n\tif got != \"[]\" {\n\t\tt.Errorf(\"Expected empty JSON array '[]', got %q\", got)\n\t}\n}\n\nfunc TestDisplayJobs_EmptyYAML(t *testing.T) {\n\tvar buf bytes.Buffer\n\terr := displayJobs([]buildkite.Job{}, output.FormatYAML, &buf)\n\tif err != nil {\n\t\tt.Fatalf(\"displayJobs failed: %v\", err)\n\t}\n\n\tgot := strings.TrimSpace(buf.String())\n\tif got != \"[]\" {\n\t\tt.Errorf(\"Expected empty YAML array '[]', got %q\", got)\n\t}\n}\n\nfunc TestFilterJobs(t *testing.T) {\n\tnow := time.Now()\n\tjobs := []buildkite.Job{\n\t\t{\n\t\t\tID:              \"job-1\",\n\t\t\tState:           \"running\",\n\t\t\tAgentQueryRules: []string{\"queue=test-queue\"},\n\t\t\tStartedAt:       &buildkite.Timestamp{Time: now.Add(-5 * time.Minute)},\n\t\t\tFinishedAt:      &buildkite.Timestamp{Time: now.Add(-4 * time.Minute)}, // 1 minute\n\t\t},\n\t\t{\n\t\t\tID:              \"job-2\",\n\t\t\tState:           \"passed\",\n\t\t\tAgentQueryRules: []string{\"queue=other-queue\"},\n\t\t\tStartedAt:       &buildkite.Timestamp{Time: now.Add(-30 * time.Minute)},\n\t\t\tFinishedAt:      &buildkite.Timestamp{Time: now.Add(-10 * time.Minute)}, // 20 minutes\n\t\t},\n\t}\n\n\topts := jobListOptions{duration: \">10m\"}\n\tfiltered, err := applyClientSideFilters(jobs, opts)\n\tif err != nil {\n\t\tt.Fatalf(\"applyClientSideFilters failed: %v\", err)\n\t}\n\n\tif len(filtered) != 1 {\n\t\tt.Errorf(\"Expected 1 job >= 10m, got %d\", len(filtered))\n\t}\n\n\topts = jobListOptions{queue: \"test-queue\"}\n\tfiltered, err = applyClientSideFilters(jobs, opts)\n\tif err != nil {\n\t\tt.Fatalf(\"applyClientSideFilters failed: %v\", err)\n\t}\n\n\tif len(filtered) != 1 {\n\t\tt.Errorf(\"Expected 1 job with 'test-queue', got %d\", len(filtered))\n\t}\n}\n"
  },
  {
    "path": "cmd/job/log.go",
    "content": "package job\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"github.com/alecthomas/kong\"\n\tbuildResolver \"github.com/buildkite/cli/v3/internal/build/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/build/resolver/options\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n)\n\ntype LogCmd struct {\n\tJobID        string `arg:\"\" help:\"Job UUID to get logs for\"`\n\tPipeline     string `help:\"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}\" short:\"p\"`\n\tBuildNumber  string `help:\"The build number\" short:\"b\"`\n\tNoTimestamps bool   `help:\"Strip timestamp prefixes from log output\" name:\"no-timestamps\"`\n}\n\nfunc (c *LogCmd) Help() string {\n\treturn `\nExamples:\n  # Get a job's logs by UUID (requires --pipeline and --build)\n  $ bk job log 0190046e-e199-453b-a302-a21a4d649d31 -p my-pipeline -b 123\n\n  # If inside a git repository with a configured pipeline\n  $ bk job log 0190046e-e199-453b-a302-a21a4d649d31 -b 123\n\n  # Strip timestamp prefixes from output\n  $ bk job log 0190046e-e199-453b-a302-a21a4d649d31 -p my-pipeline -b 123 --no-timestamps\n`\n}\n\nfunc (c *LogCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\tpipelineRes := pipelineResolver.NewAggregateResolver(\n\t\tpipelineResolver.ResolveFromFlag(c.Pipeline, f.Config),\n\t\tpipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)),\n\t\tpipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOneWithFactory(f))),\n\t)\n\n\toptionsResolver := options.AggregateResolver{\n\t\toptions.ResolveBranchFromRepository(f.GitRepository),\n\t}\n\n\targs := []string{}\n\tif c.BuildNumber != \"\" {\n\t\targs = []string{c.BuildNumber}\n\t}\n\tbuildRes := buildResolver.NewAggregateResolver(\n\t\tbuildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config),\n\t\tbuildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...),\n\t)\n\n\tbld, err := buildRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif bld == nil {\n\t\treturn fmt.Errorf(\"no build found\")\n\t}\n\n\tvar logContent string\n\tif err = bkIO.SpinWhile(f, \"Fetching job log\", func() error {\n\t\tjobLog, _, apiErr := f.RestAPIClient.Jobs.GetJobLog(\n\t\t\tctx,\n\t\t\tbld.Organization,\n\t\t\tbld.Pipeline,\n\t\t\tfmt.Sprint(bld.BuildNumber),\n\t\t\tc.JobID,\n\t\t)\n\t\tif apiErr != nil {\n\t\t\treturn apiErr\n\t\t}\n\t\tlogContent = jobLog.Content\n\t\treturn nil\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tif c.NoTimestamps {\n\t\tlogContent = stripTimestamps(logContent)\n\t}\n\n\twriter, cleanup := bkIO.Pager(f.NoPager)\n\tdefer func() { _ = cleanup() }()\n\n\tfmt.Fprint(writer, logContent)\n\treturn nil\n}\n\nvar timestampRegex = regexp.MustCompile(`bk;t=\\d+\\x07`)\n\nfunc stripTimestamps(content string) string {\n\treturn timestampRegex.ReplaceAllString(content, \"\")\n}\n"
  },
  {
    "path": "cmd/job/reprioritize.go",
    "content": "package job\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/alecthomas/kong\"\n\tbuildResolver \"github.com/buildkite/cli/v3/internal/build/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/build/resolver/options\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype ReprioritizeCmd struct {\n\tJobID       string `arg:\"\" help:\"Job UUID to reprioritize\"`\n\tPriority    int    `arg:\"\" help:\"New priority value for the job\"`\n\tPipeline    string `help:\"The pipeline to use. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}\" short:\"p\"`\n\tBuildNumber string `help:\"The build number\" short:\"b\"`\n}\n\nfunc (c *ReprioritizeCmd) Help() string {\n\treturn `\nExamples:\n  # Reprioritize a job (requires --pipeline and --build)\n  $ bk job reprioritize 0190046e-e199-453b-a302-a21a4d649d31 1 -p my-pipeline -b 123\n\n  # If inside a git repository with a configured pipeline\n  $ bk job reprioritize 0190046e-e199-453b-a302-a21a4d649d31 1 -b 123\n`\n}\n\nfunc (c *ReprioritizeCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\tpipelineRes := pipelineResolver.NewAggregateResolver(\n\t\tpipelineResolver.ResolveFromFlag(c.Pipeline, f.Config),\n\t\tpipelineResolver.ResolveFromConfig(f.Config, pipelineResolver.PickOneWithFactory(f)),\n\t\tpipelineResolver.ResolveFromRepository(f, pipelineResolver.CachedPicker(f.Config, pipelineResolver.PickOneWithFactory(f))),\n\t)\n\n\toptionsResolver := options.AggregateResolver{\n\t\toptions.ResolveBranchFromRepository(f.GitRepository),\n\t}\n\n\targs := []string{}\n\tif c.BuildNumber != \"\" {\n\t\targs = []string{c.BuildNumber}\n\t}\n\tbuildRes := buildResolver.NewAggregateResolver(\n\t\tbuildResolver.ResolveFromPositionalArgument(args, 0, pipelineRes.Resolve, f.Config),\n\t\tbuildResolver.ResolveBuildWithOpts(f, pipelineRes.Resolve, optionsResolver...),\n\t)\n\n\tbld, err := buildRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif bld == nil {\n\t\treturn fmt.Errorf(\"no build found\")\n\t}\n\n\tvar job buildkite.Job\n\tif err = bkIO.SpinWhile(f, \"Reprioritizing job\", func() error {\n\t\tvar apiErr error\n\t\tjob, _, apiErr = f.RestAPIClient.Jobs.ReprioritizeJob(\n\t\t\tctx,\n\t\t\tbld.Organization,\n\t\t\tbld.Pipeline,\n\t\t\tfmt.Sprint(bld.BuildNumber),\n\t\t\tc.JobID,\n\t\t\t&buildkite.JobReprioritizationOptions{\n\t\t\t\tPriority: c.Priority,\n\t\t\t},\n\t\t)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Printf(\"Job reprioritized to %d: %s\\n\", c.Priority, job.WebURL)\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/job/retry.go",
    "content": "package job\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkGraphQL \"github.com/buildkite/cli/v3/internal/graphql\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/internal/util\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n)\n\nconst jobCommandPrefix = \"JobTypeCommand---\"\n\ntype RetryCmd struct {\n\tJobID string `arg:\"\" help:\"Job UUID to retry\"`\n}\n\nfunc (c *RetryCmd) Help() string {\n\treturn `Use this command to retry build jobs.\n\nExamples:\n  # Retry a job by UUID\n  $ bk job retry 0190046e-e199-453b-a302-a21a4d649d31\n`\n}\n\nfunc (c *RetryCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\t// Given a job UUID argument, we need to generate the GraphQL ID matching\n\tgraphqlID := util.GenerateGraphQLID(jobCommandPrefix, c.JobID)\n\n\tctx := context.Background()\n\tvar j *bkGraphQL.RetryJobResponse\n\n\tif err = bkIO.SpinWhile(f, \"Retrying job\", func() error {\n\t\tj, err = bkGraphQL.RetryJob(ctx, f.GraphQLClient, graphqlID)\n\t\treturn err\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\t// Fixes segfault when error is returned, e.g. \"Jobs from canceled builds cannot be retried\"\n\tif j == nil || j.JobTypeCommandRetry == nil {\n\t\treturn fmt.Errorf(\"failed to retry job\")\n\t}\n\n\tfmt.Println(\"Successfully retried job: \" + j.JobTypeCommandRetry.JobTypeCommand.Url)\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/job/unblock.go",
    "content": "package job\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkGraphQL \"github.com/buildkite/cli/v3/internal/graphql\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/internal/util\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/vektah/gqlparser/v2/gqlerror\"\n)\n\nconst jobBlockPrefix = \"JobTypeBlock---\"\n\ntype UnblockCmd struct {\n\tJobID string `arg:\"\" help:\"Job UUID to unblock\"`\n\tData  string `help:\"JSON formatted data to unblock the job\"`\n}\n\nfunc (c *UnblockCmd) Help() string {\n\treturn `\nUnblock a job.\n\nUse this command to unblock build jobs.\nCurrently, this does not support submitting fields to the step.\n\nExamples:\n  # Unblock a job by UUID\n  $ bk job unblock 0190046e-e199-453b-a302-a21a4d649d31\n\n  # Unblock with JSON data\n  $ bk job unblock 0190046e-e199-453b-a302-a21a4d649d31 --data '{\"field\": \"value\"}'\n\n  # Unblock with data from stdin\n  $ echo '{\"field\": \"value\"}' | bk job unblock 0190046e-e199-453b-a302-a21a4d649d31\n`\n}\n\nfunc (c *UnblockCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\t// Given a job UUID argument, we need to generate the GraphQL ID matching\n\tgraphqlID := util.GenerateGraphQLID(jobBlockPrefix, c.JobID)\n\n\t// Get unblock step fields if available\n\tvar fields *string\n\tif bkIO.HasDataAvailable(os.Stdin) {\n\t\tstdin := new(strings.Builder)\n\t\t_, err := io.Copy(stdin, os.Stdin)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tinput := stdin.String()\n\t\tfields = &input\n\t} else if c.Data != \"\" {\n\t\tfields = &c.Data\n\t} else {\n\t\t// The GraphQL API errors if providing a null fields value so we need to provide an empty json object\n\t\tinput := \"{}\"\n\t\tfields = &input\n\t}\n\n\tctx := context.Background()\n\tvar result *bkGraphQL.UnblockJobResponse\n\terr = bkIO.SpinWhile(f, \"Unblocking job\", func() error {\n\t\tresult, err = bkGraphQL.UnblockJob(ctx, f.GraphQLClient, graphqlID, fields)\n\t\treturn err\n\t})\n\tif err != nil {\n\t\t// Handle a \"graphql error\" if the job is already unblocked\n\t\tvar errList gqlerror.List\n\t\tif errors.As(err, &errList) {\n\t\t\tfor _, gqlErr := range errList {\n\t\t\t\tif gqlErr.Message == \"The job's state must be blocked\" {\n\t\t\t\t\tfmt.Println(\"This job is already unblocked\")\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn err\n\t}\n\n\tif err := validateUnblockResponse(result); err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Println(\"Successfully unblocked job\")\n\treturn nil\n}\n\nfunc validateUnblockResponse(result *bkGraphQL.UnblockJobResponse) error {\n\tif result == nil || result.JobTypeBlockUnblock == nil {\n\t\treturn fmt.Errorf(\"failed to unblock job\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/job/unblock_test.go",
    "content": "package job\n\nimport (\n\t\"testing\"\n\n\tbkGraphQL \"github.com/buildkite/cli/v3/internal/graphql\"\n)\n\nfunc TestValidateUnblockResponse(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname    string\n\t\tinput   *bkGraphQL.UnblockJobResponse\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"nil response\",\n\t\t\tinput:   nil,\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"nil payload\",\n\t\t\tinput: &bkGraphQL.UnblockJobResponse{\n\t\t\t\tJobTypeBlockUnblock: nil,\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"successful unblock\",\n\t\t\tinput: &bkGraphQL.UnblockJobResponse{\n\t\t\t\tJobTypeBlockUnblock: &bkGraphQL.UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayload{\n\t\t\t\t\tJobTypeBlock: bkGraphQL.UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock{\n\t\t\t\t\t\tState: bkGraphQL.JobStatesUnblocked,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"non-nil payload with finished state\",\n\t\t\tinput: &bkGraphQL.UnblockJobResponse{\n\t\t\t\tJobTypeBlockUnblock: &bkGraphQL.UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayload{\n\t\t\t\t\tJobTypeBlock: bkGraphQL.UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock{\n\t\t\t\t\t\tState: bkGraphQL.JobStatesFinished,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\terr := validateUnblockResponse(tt.input)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"validateUnblockResponse() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/maintainer/create.go",
    "content": "package maintainer\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype CreateCmd struct {\n\tClusterUUID string `arg:\"\" help:\"Cluster UUID to add maintainer to\" name:\"cluster-uuid\"`\n\tUser        string `help:\"User UUID to add as maintainer\" optional:\"\" xor:\"actor\"`\n\tTeam        string `help:\"Team UUID to add as maintainer\" optional:\"\" xor:\"actor\"`\n\toutput.OutputFlags\n}\n\nfunc (c *CreateCmd) Help() string {\n\treturn `\nCreate a cluster maintainer.\n\nEither --user or --team must be specified.\n\nExamples:\n\t# Create a user maintainer assignment\n  $ bk maintainer create my-cluster-uuid --user user-uuid\n\n\t# Create a team maintainer assignment\n  $ bk maintainer create my-cluster-uuid --team team-uuid\n`\n}\n\nfunc (c *CreateCmd) Validate() error {\n\tif c.User == \"\" && c.Team == \"\" {\n\t\treturn fmt.Errorf(\"either --user or --team must be specified\")\n\t}\n\treturn nil\n}\n\nfunc (c *CreateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tinput := buildkite.ClusterMaintainer{}\n\tif c.User != \"\" {\n\t\tinput.UserID = c.User\n\t} else {\n\t\tinput.TeamID = c.Team\n\t}\n\n\tvar maintainer buildkite.ClusterMaintainerEntry\n\tif err = bkIO.SpinWhile(f, \"Creating cluster maintainer\", func() error {\n\t\tvar apiErr error\n\t\tmaintainer, _, apiErr = f.RestAPIClient.ClusterMaintainers.Create(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, input)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error creating cluster maintainer: %v\", err)\n\t}\n\n\tmaintainerView := output.Viewable[buildkite.ClusterMaintainerEntry]{\n\t\tData:   maintainer,\n\t\tRender: renderMaintainerText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, maintainerView, format)\n\t}\n\n\tfmt.Fprintf(os.Stdout, \"Maintainer created successfully\\n\\n\")\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\treturn output.Write(writer, maintainerView, format)\n}\n\nfunc renderMaintainerText(m buildkite.ClusterMaintainerEntry) string {\n\tname := m.Actor.Slug\n\tif m.Actor.Name != \"\" {\n\t\tname = m.Actor.Name\n\t}\n\n\trows := [][]string{\n\t\t{\"Assignment ID\", output.ValueOrDash(m.ID)},\n\t\t{\"Actor ID\", output.ValueOrDash(m.Actor.ID)},\n\t\t{\"Type\", output.ValueOrDash(m.Actor.Type)},\n\t\t{\"Name\", output.ValueOrDash(name)},\n\t}\n\n\ttable := output.Table(\n\t\t[]string{\"Field\", \"Value\"},\n\t\trows,\n\t\tmap[string]string{\"field\": \"dim\", \"value\": \"italic\"},\n\t)\n\n\tvar sb strings.Builder\n\tfmt.Fprintf(&sb, \"Maintainer assignment %s\\n\\n\", output.ValueOrDash(m.ID))\n\tsb.WriteString(table)\n\n\treturn sb.String()\n}\n"
  },
  {
    "path": "cmd/maintainer/delete.go",
    "content": "package maintainer\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n)\n\ntype DeleteCmd struct {\n\tClusterUUID  string `arg:\"\" help:\"Cluster UUID\" name:\"cluster-uuid\"`\n\tMaintainerID string `arg:\"\" help:\"Maintainer assignment ID to delete\" name:\"maintainer-id\"`\n}\n\nfunc (c *DeleteCmd) Help() string {\n\treturn `\nDelete a cluster maintainer.\n\nYou will be prompted to confirm deletion unless --yes is set.\n\nExamples:\n\t# Delete a maintainer assignment (with confirmation prompt)\n  $ bk maintainer delete my-cluster-uuid maintainer-id\n\n  # Delete without confirmation\n  $ bk maintainer delete my-cluster-uuid maintainer-id --yes\n\n\t# Use list to find maintainer assignment IDs\n  $ bk maintainer list my-cluster-uuid\n`\n}\n\nfunc (c *DeleteCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tconfirmed, err := bkIO.Confirm(f, fmt.Sprintf(\"Are you sure you want to delete maintainer %s?\", c.MaintainerID))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !confirmed {\n\t\tfmt.Fprintln(os.Stderr, \"Deletion cancelled.\")\n\t\treturn nil\n\t}\n\n\tif err = bkIO.SpinWhile(f, \"Deleting cluster maintainer\", func() error {\n\t\t_, apiErr := f.RestAPIClient.ClusterMaintainers.Delete(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, c.MaintainerID)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error deleting cluster maintainer: %v\", err)\n\t}\n\n\tfmt.Fprintln(os.Stderr, \"Maintainer deleted successfully.\")\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/maintainer/list.go",
    "content": "package maintainer\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype ListCmd struct {\n\tClusterUUID string `arg:\"\" help:\"Cluster UUID to list maintainers for\" name:\"cluster-uuid\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ListCmd) Help() string {\n\treturn `\nList cluster maintainers.\n\nExamples:\n  # List all maintainers for a cluster\n  $ bk maintainer list my-cluster-uuid\n\n  # List in JSON format\n  $ bk maintainer list my-cluster-uuid -o json\n`\n}\n\nfunc (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tvar maintainers []buildkite.ClusterMaintainerEntry\n\tif err = bkIO.SpinWhile(f, \"Fetching cluster maintainers\", func() error {\n\t\tvar apiErr error\n\t\tmaintainers, _, apiErr = f.RestAPIClient.ClusterMaintainers.List(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, nil)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error fetching cluster maintainers: %v\", err)\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, maintainers, format)\n\t}\n\n\tif len(maintainers) == 0 {\n\t\tfmt.Fprintln(os.Stdout, \"No maintainers found\")\n\t\treturn nil\n\t}\n\n\trows := make([][]string, 0, len(maintainers))\n\tfor _, m := range maintainers {\n\t\tname := m.Actor.Slug\n\t\tif m.Actor.Name != \"\" {\n\t\t\tname = m.Actor.Name\n\t\t}\n\n\t\trows = append(rows, []string{m.ID, m.Actor.ID, m.Actor.Type, name})\n\t}\n\n\ttable := output.Table(\n\t\t[]string{\"Assignment ID\", \"Actor ID\", \"Type\", \"Name\"},\n\t\trows,\n\t\tmap[string]string{\"assignment id\": \"bold\", \"name\": \"italic\"},\n\t)\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\t_, err = fmt.Fprintf(writer, \"Maintainers (%d)\\n\\n%s\\n\", len(maintainers), table)\n\treturn err\n}\n"
  },
  {
    "path": "cmd/maintainer/maintainer_test.go",
    "content": "package maintainer\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestListMaintainers(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"fetches maintainers through API\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tmaintainers := []buildkite.ClusterMaintainerEntry{\n\t\t\t{\n\t\t\t\tID: \"maintainer-1\",\n\t\t\t\tActor: buildkite.ClusterMaintainerActor{\n\t\t\t\t\tType: \"user\",\n\t\t\t\t\tName: \"Jurgen Klopp\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tID: \"maintainer-2\",\n\t\t\t\tActor: buildkite.ClusterMaintainerActor{\n\t\t\t\t\tType: \"team\",\n\t\t\t\t\tSlug: \"platform\",\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != \"GET\" {\n\t\t\t\tt.Errorf(\"expected GET, got %s\", r.Method)\n\t\t\t}\n\t\t\tif !strings.Contains(r.URL.Path, \"/clusters/cluster-123/maintainers\") {\n\t\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t\t}\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(maintainers)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tresult, _, err := client.ClusterMaintainers.List(context.Background(), \"test-org\", \"cluster-123\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(result) != 2 {\n\t\t\tt.Fatalf(\"expected 2 maintainers, got %d\", len(result))\n\t\t}\n\n\t\tif result[0].Actor.Name != \"Jurgen Klopp\" {\n\t\t\tt.Errorf(\"expected name 'Jurgen Klopp', got %q\", result[0].Actor.Name)\n\t\t}\n\n\t\tif result[1].Actor.Slug != \"platform\" {\n\t\t\tt.Errorf(\"expected slug 'platform', got %q\", result[1].Actor.Slug)\n\t\t}\n\t})\n\n\tt.Run(\"empty result returns empty slice\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode([]buildkite.ClusterMaintainerEntry{})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tresult, _, err := client.ClusterMaintainers.List(context.Background(), \"test-org\", \"cluster-123\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(result) != 0 {\n\t\t\tt.Errorf(\"expected 0 maintainers, got %d\", len(result))\n\t\t}\n\t})\n}\n\nfunc TestCreateMaintainer(t *testing.T) {\n\tt.Parallel()\n\n\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != \"POST\" {\n\t\t\tt.Errorf(\"expected POST, got %s\", r.Method)\n\t\t}\n\t\tif !strings.Contains(r.URL.Path, \"/clusters/cluster-123/maintainers\") {\n\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t}\n\n\t\tvar input buildkite.ClusterMaintainer\n\t\tif err := json.NewDecoder(r.Body).Decode(&input); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif input.UserID != \"user-123\" {\n\t\t\tt.Errorf(\"expected user id 'user-123', got %q\", input.UserID)\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(http.StatusCreated)\n\t\tjson.NewEncoder(w).Encode(buildkite.ClusterMaintainerEntry{\n\t\t\tID: \"maintainer-123\",\n\t\t\tActor: buildkite.ClusterMaintainerActor{\n\t\t\t\tType: \"user\",\n\t\t\t\tName: \"Jurgen Klopp\",\n\t\t\t},\n\t\t})\n\t}))\n\tdefer s.Close()\n\n\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tresult, _, err := client.ClusterMaintainers.Create(context.Background(), \"test-org\", \"cluster-123\", buildkite.ClusterMaintainer{UserID: \"user-123\"})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif result.ID != \"maintainer-123\" {\n\t\tt.Errorf(\"expected ID 'maintainer-123', got %q\", result.ID)\n\t}\n\n\tif result.Actor.Type != \"user\" {\n\t\tt.Errorf(\"expected actor type 'user', got %q\", result.Actor.Type)\n\t}\n}\n\nfunc TestDeleteMaintainer(t *testing.T) {\n\tt.Parallel()\n\n\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != \"DELETE\" {\n\t\t\tt.Errorf(\"expected DELETE, got %s\", r.Method)\n\t\t}\n\t\tif !strings.Contains(r.URL.Path, \"/maintainers/maintainer-123\") {\n\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t}\n\t\tw.WriteHeader(http.StatusNoContent)\n\t}))\n\tdefer s.Close()\n\n\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t_, err = client.ClusterMaintainers.Delete(context.Background(), \"test-org\", \"cluster-123\", \"maintainer-123\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/organization/list.go",
    "content": "package organization\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n)\n\ntype ListCmd struct {\n\toutput.OutputFlags\n}\n\ntype Organization struct {\n\tSlug     string `json:\"slug\" yaml:\"slug\"`\n\tSelected bool   `json:\"selected\" yaml:\"selected\"`\n}\n\nfunc (c *ListCmd) Help() string {\n\treturn `List configured organizations.\n\nExamples:\n  # List all configured organizations (JSON by default)\n  $ bk organization list\n\n  # List organizations in text format\n  $ bk organization list -o text\n`\n}\n\nfunc (c *ListCmd) Run(globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\torgs := f.Config.ConfiguredOrganizations()\n\tif len(orgs) == 0 {\n\t\treturn output.WriteTextOrStructured(os.Stdout, format, []Organization{}, \"No organizations configured. Run `bk configure` to add one.\")\n\t}\n\n\tslices.Sort(orgs)\n\tselectedOrg := f.Config.OrganizationSlug()\n\n\torganizations := make([]Organization, len(orgs))\n\tfor i, org := range orgs {\n\t\torganizations[i] = Organization{\n\t\t\tSlug:     org,\n\t\t\tSelected: org == selectedOrg,\n\t\t}\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, organizations, format)\n\t}\n\n\trows := make([][]string, 0, len(organizations))\n\tfor _, org := range organizations {\n\t\trows = append(rows, []string{org.Slug, strconv.FormatBool(org.Selected)})\n\t}\n\n\ttable := output.Table(\n\t\t[]string{\"Organization Slug\", \"Selected\"},\n\t\trows,\n\t\tmap[string]string{\"organization slug\": \"bold\", \"selected\": \"italic\"},\n\t)\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\tfmt.Fprintf(writer, \"Showing configured organization(s)\\n\\n%s\\n\", table)\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/pipeline/convert.go",
    "content": "package pipeline\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\nconst convertEndpoint = \"https://m4vrh5pvtd.execute-api.us-east-1.amazonaws.com/production/migrate\"\n\ntype conversionRequest struct {\n\tVendor string `json:\"vendor\"`\n\tCode   string `json:\"code\"`\n}\n\ntype conversionResponse struct {\n\tJobID     string `json:\"jobId\"`\n\tStatus    string `json:\"status\"`\n\tMessage   string `json:\"message\"`\n\tStatusURL string `json:\"statusUrl\"`\n}\n\ntype statusResponse struct {\n\tJobID       string `json:\"jobId\"`\n\tStatus      string `json:\"status\"`\n\tVendor      string `json:\"vendor\"`\n\tCreatedAt   string `json:\"createdAt\"`\n\tCompletedAt string `json:\"completedAt,omitempty\"`\n\tResult      string `json:\"result,omitempty\"`\n\tError       string `json:\"error,omitempty\"`\n}\n\ntype ConvertCmd struct {\n\tFile    string `help:\"Path to the pipeline file to convert (required)\" short:\"F\" required:\"\"`\n\tVendor  string `help:\"CI/CD vendor (auto-detected if the file name matches vendor path and name - otherwise, needs to be specified)\" short:\"v\"`\n\tOutput  string `help:\"Custom path to save the converted pipeline (default: .buildkite/pipeline.<vendor>.yml)\" short:\"o\"`\n\tTimeout int    `help:\"The time (in seconds) after which a conversion should be cancelled\" default:\"300\"`\n}\n\nfunc (c *ConvertCmd) Help() string {\n\treturn `\nSupported vendors:\n  - github (GitHub Actions)\n  - bitbucket (Bitbucket Pipelines)\n  - circleci (CircleCI)\n  - jenkins (Jenkins)\n  - gitlab (GitLab CI) (beta)\n  - harness (Harness CI) (beta)\n  - bitrise (Bitrise) (beta)\n\nThe command will automatically detect the vendor based on the file path and name if not specified.\n\nWhen using --file, the converted pipeline is saved to .buildkite/pipeline.<vendor>.yml by default.\nWhen reading from stdin, output goes to stdout by default.\nUse the --output flag to specify a custom output path in either case.\n\nNote: This command does not require an API token since it uses a public conversion API.\n\nExamples:\n  # Convert a GitHub Actions workflow\n  $ bk pipeline convert -F .github/workflows/ci.yml\n\n  # Convert with explicit vendor specification\n  $ bk pipeline convert -F pipeline.yml --vendor circleci\n\n  # Save output to a file\n  $ bk pipeline convert -F .github/workflows/ci.yml -o .buildkite/pipeline.yml\n\n  # Read from stdin\n  $ cat .github/workflows/ci.yml | bk pipeline convert --vendor github\n  $ bk pipeline convert --vendor github < .github/workflows/ci.yml\n`\n}\n\nfunc (c *ConvertCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tfromStdin := c.File == \"\"\n\n\tvar content []byte\n\tif fromStdin {\n\t\tif !bkIO.HasDataAvailable(os.Stdin) {\n\t\t\treturn errors.New(\"no input: provide a file with --file or pipe content via stdin\")\n\t\t}\n\t\tif c.Vendor == \"\" {\n\t\t\treturn errors.New(\"--vendor is required when reading from stdin\")\n\t\t}\n\t\tcontent, err = io.ReadAll(os.Stdin)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading stdin: %w\", err)\n\t\t}\n\t} else {\n\t\tcontent, err = os.ReadFile(c.File)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading file: %w\", err)\n\t\t}\n\t}\n\n\tif c.Vendor == \"\" {\n\t\tc.Vendor, err = detectVendor(c.File)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfmt.Printf(\"Detected vendor: %s\\n\", c.Vendor)\n\t}\n\n\tsupportedVendors := []string{\"github\", \"bitbucket\", \"circleci\", \"jenkins\", \"gitlab\", \"harness\", \"bitrise\"}\n\tif !slices.Contains(supportedVendors, c.Vendor) {\n\t\treturn fmt.Errorf(\"unsupported vendor: %s (supported: %s)\", c.Vendor, strings.Join(supportedVendors, \", \"))\n\t}\n\n\tif c.Timeout < 1 {\n\t\treturn errors.New(\"a timeout cannot be less than 1 second\")\n\t}\n\n\treq := conversionRequest{\n\t\tVendor: c.Vendor,\n\t\tCode:   string(content),\n\t}\n\n\tfmt.Println(\"Submitting conversion job...\")\n\n\tjobResp, err := submitConversionJob(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error submitting conversion job: %w\", err)\n\t}\n\n\tfmt.Println(\"Job submitted. Processing with AI (this may take several minutes)...\")\n\n\tvar result *statusResponse\n\tif err = bkIO.SpinWhile(f, \"Processing conversion...\", func() error {\n\t\tvar pollErr error\n\t\tresult, pollErr = pollJobStatus(jobResp.JobID, c.Timeout)\n\t\treturn pollErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error polling job status: %w\", err)\n\t}\n\n\tif result.Status == \"failed\" {\n\t\treturn fmt.Errorf(\"conversion failed: %s\", result.Error)\n\t}\n\n\tif c.Output != \"\" {\n\t\tif err := os.WriteFile(c.Output, []byte(result.Result), 0o644); err != nil {\n\t\t\treturn fmt.Errorf(\"error writing output file: %w\", err)\n\t\t}\n\t\tfmt.Printf(\"\\n✅ conversion completed successfully!\\n\")\n\t\tfmt.Printf(\"Output saved to: %s\\n\", c.Output)\n\t} else if fromStdin {\n\t\tfmt.Print(result.Result)\n\t} else {\n\t\tbuildkiteDir := \".buildkite\"\n\t\tif err := os.MkdirAll(buildkiteDir, 0o755); err != nil {\n\t\t\treturn fmt.Errorf(\"error creating .buildkite directory: %w\", err)\n\t\t}\n\n\t\toutputFilename := fmt.Sprintf(\"pipeline.%s.yml\", c.Vendor)\n\t\tdefaultOutputPath := filepath.Join(buildkiteDir, outputFilename)\n\n\t\tif err := os.WriteFile(defaultOutputPath, []byte(result.Result), 0o644); err != nil {\n\t\t\treturn fmt.Errorf(\"error writing output file: %w\", err)\n\t\t}\n\n\t\tfmt.Printf(\"\\n✅ conversion completed successfully!\\n\")\n\t\tfmt.Printf(\"Output saved to: %s\\n\", defaultOutputPath)\n\t}\n\n\treturn nil\n}\n\nfunc detectVendor(filePath string) (string, error) {\n\tfileName := filepath.Base(filePath)\n\n\tif strings.Contains(filePath, \".github/workflows\") || strings.Contains(filePath, \".github\\\\workflows\") {\n\t\treturn \"github\", nil\n\t}\n\n\tif fileName == \"bitbucket-pipelines.yml\" || fileName == \"bitbucket-pipelines.yaml\" {\n\t\treturn \"bitbucket\", nil\n\t}\n\n\tif strings.Contains(filePath, \".circleci\") {\n\t\treturn \"circleci\", nil\n\t}\n\n\tif fileName == \"Jenkinsfile\" || strings.HasPrefix(fileName, \"Jenkinsfile.\") {\n\t\treturn \"jenkins\", nil\n\t}\n\n\treturn \"\", fmt.Errorf(\"could not detect vendor from file path. Please specify vendor explicitly with --vendor\")\n}\n\nfunc submitConversionJob(req conversionRequest) (*conversionResponse, error) {\n\treturn submitConversionJobAtEndpoint(convertEndpoint, req)\n}\n\nfunc submitConversionJobAtEndpoint(endpoint string, req conversionRequest) (*conversionResponse, error) {\n\treqBody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshaling request: %w\", err)\n\t}\n\n\thttpReq, err := http.NewRequestWithContext(context.Background(), http.MethodPost, endpoint, bytes.NewReader(reqBody))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error creating request: %w\", err)\n\t}\n\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tresp, err := client.Do(httpReq)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error making request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"API request failed (status %d): %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar jobResp conversionResponse\n\tif err := json.Unmarshal(body, &jobResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing response: %w\", err)\n\t}\n\n\treturn &jobResp, nil\n}\n\nfunc pollJobStatus(jobID string, timeoutSeconds int) (*statusResponse, error) {\n\tmaxAttempts := timeoutSeconds / 5\n\tif maxAttempts < 1 {\n\t\tmaxAttempts = 1\n\t}\n\n\treturn pollJobStatusWithConfig(jobID, pollConfig{\n\t\tendpoint:    convertEndpoint,\n\t\tclient:      &http.Client{Timeout: 30 * time.Second},\n\t\tmaxAttempts: maxAttempts,\n\t\tinterval:    5 * time.Second,\n\t\ttimeout:     time.Duration(timeoutSeconds) * time.Second,\n\t})\n}\n\ntype pollConfig struct {\n\tendpoint    string\n\tclient      *http.Client\n\tmaxAttempts int\n\tinterval    time.Duration\n\ttimeout     time.Duration\n}\n\nfunc pollJobStatusWithConfig(jobID string, cfg pollConfig) (*statusResponse, error) {\n\tif cfg.endpoint == \"\" {\n\t\tcfg.endpoint = convertEndpoint\n\t}\n\tif cfg.client == nil {\n\t\tcfg.client = &http.Client{Timeout: 30 * time.Second}\n\t}\n\tif cfg.maxAttempts < 1 {\n\t\tcfg.maxAttempts = 1\n\t}\n\tif cfg.interval < 0 {\n\t\tcfg.interval = 0\n\t}\n\tif cfg.timeout < 0 {\n\t\tcfg.timeout = 0\n\t}\n\n\tstatusURL := fmt.Sprintf(\"%s/%s/status\", cfg.endpoint, jobID)\n\n\tfor attempt := 0; attempt < cfg.maxAttempts; attempt++ {\n\t\tif attempt > 0 && cfg.interval > 0 {\n\t\t\ttime.Sleep(cfg.interval)\n\t\t}\n\n\t\treq, err := http.NewRequestWithContext(context.Background(), http.MethodGet, statusURL, nil)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error creating status request: %w\", err)\n\t\t}\n\n\t\tresp, err := cfg.client.Do(req)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error checking status: %w\", err)\n\t\t}\n\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error reading status response: %w\", err)\n\t\t}\n\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn nil, fmt.Errorf(\"status check failed (status %d): %s\", resp.StatusCode, string(body))\n\t\t}\n\n\t\tvar status statusResponse\n\t\tif err := json.Unmarshal(body, &status); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error parsing status response: %w\", err)\n\t\t}\n\n\t\tif status.Status == \"completed\" {\n\t\t\treturn &status, nil\n\t\t}\n\n\t\tif status.Status == \"failed\" {\n\t\t\treturn &status, nil\n\t\t}\n\t}\n\n\tif cfg.timeout > 0 {\n\t\treturn nil, fmt.Errorf(\"conversion timed out after %d seconds\", int(cfg.timeout/time.Second))\n\t}\n\n\treturn nil, fmt.Errorf(\"conversion timed out after %d attempts\", cfg.maxAttempts)\n}\n"
  },
  {
    "path": "cmd/pipeline/convert_test.go",
    "content": "package pipeline\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nconst conversionIntegrationTestsEnv = \"BK_RUN_INTEGRATION_TESTS\"\n\nfunc TestConversionAPIEndpoint(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Skipping integration test in short mode\")\n\t}\n\tif os.Getenv(conversionIntegrationTestsEnv) == \"\" {\n\t\tt.Skipf(\"Skipping external conversion API test; set %s=1 to run it\", conversionIntegrationTestsEnv)\n\t}\n\n\t// Create a simple GitHub Actions workflow for testing\n\ttestWorkflow := `name: Test\non: [push]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - run: echo \"Hello World\"\n`\n\n\t// Submit a Conversion job\n\treq := conversionRequest{\n\t\tVendor: \"github\",\n\t\tCode:   testWorkflow,\n\t}\n\n\tjobResp, err := submitConversionJob(req)\n\tif err != nil {\n\t\tt.Fatalf(\"Conversion API endpoint is not accessible or broken. This will break the CLI for users. Error: %v\", err)\n\t}\n\n\tif jobResp.JobID == \"\" {\n\t\tt.Error(\"Expected job ID to be returned\")\n\t}\n\n\tif jobResp.Status != \"processing\" && jobResp.Status != \"queued\" {\n\t\tt.Errorf(\"Expected status to be 'processing' or 'queued', got: %s\", jobResp.Status)\n\t}\n\n\t// Poll for completion with a reasonable timeout\n\tresult, err := pollJobStatus(jobResp.JobID, 60)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to poll job status: %v\", err)\n\t}\n\n\tif result.Status == \"failed\" {\n\t\tt.Errorf(\"Conversion failed: %s\", result.Error)\n\t}\n\n\tif result.Status != \"completed\" {\n\t\tt.Errorf(\"Expected status to be 'completed', got: %s\", result.Status)\n\t}\n\n\tif result.Result == \"\" {\n\t\tt.Error(\"Expected result to contain migrated pipeline YAML\")\n\t}\n\n\t// Verify the result is valid YAML\n\tif !strings.Contains(result.Result, \"steps:\") {\n\t\tt.Errorf(\"Expected result to contain 'steps:', got: %s\", result.Result)\n\t}\n}\n\nfunc TestDetectVendor(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname       string\n\t\tfilePath   string\n\t\twantVendor string\n\t\twantErr    bool\n\t}{\n\t\t{\n\t\t\tname:       \"GitHub Actions workflow\",\n\t\t\tfilePath:   \".github/workflows/ci.yml\",\n\t\t\twantVendor: \"github\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"GitHub Actions workflow (Windows path)\",\n\t\t\tfilePath:   \".github\\\\workflows\\\\ci.yml\",\n\t\t\twantVendor: \"github\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Bitbucket Pipelines\",\n\t\t\tfilePath:   \"bitbucket-pipelines.yml\",\n\t\t\twantVendor: \"bitbucket\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Bitbucket Pipelines (yaml extension)\",\n\t\t\tfilePath:   \"bitbucket-pipelines.yaml\",\n\t\t\twantVendor: \"bitbucket\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"CircleCI config\",\n\t\t\tfilePath:   \".circleci/config.yml\",\n\t\t\twantVendor: \"circleci\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Jenkins file\",\n\t\t\tfilePath:   \"Jenkinsfile\",\n\t\t\twantVendor: \"jenkins\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Jenkins file with extension\",\n\t\t\tfilePath:   \"Jenkinsfile.production\",\n\t\t\twantVendor: \"jenkins\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:     \"Unknown file\",\n\t\t\tfilePath: \"some-random-file.yml\",\n\t\t\twantErr:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvendor, err := detectVendor(tt.filePath)\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"Expected error but got none\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif vendor != tt.wantVendor {\n\t\t\t\tt.Errorf(\"Expected vendor %q, got %q\", tt.wantVendor, vendor)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestContains(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname  string\n\t\tslice []string\n\t\tstr   string\n\t\twant  bool\n\t}{\n\t\t{\n\t\t\tname:  \"string present\",\n\t\t\tslice: []string{\"github\", \"bitbucket\", \"circleci\"},\n\t\t\tstr:   \"github\",\n\t\t\twant:  true,\n\t\t},\n\t\t{\n\t\t\tname:  \"string not present\",\n\t\t\tslice: []string{\"github\", \"bitbucket\", \"circleci\"},\n\t\t\tstr:   \"jenkins\",\n\t\t\twant:  false,\n\t\t},\n\t\t{\n\t\t\tname:  \"empty slice\",\n\t\t\tslice: []string{},\n\t\t\tstr:   \"github\",\n\t\t\twant:  false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgot := slices.Contains(tt.slice, tt.str)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", tt.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSubmitConversionJob(t *testing.T) {\n\tt.Parallel()\n\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != \"POST\" {\n\t\t\tt.Errorf(\"Expected POST request, got %s\", r.Method)\n\t\t}\n\n\t\tif r.Header.Get(\"Content-Type\") != \"application/json\" {\n\t\t\tt.Errorf(\"Expected Content-Type: application/json, got %s\", r.Header.Get(\"Content-Type\"))\n\t\t}\n\n\t\tvar req conversionRequest\n\t\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n\t\t\tt.Errorf(\"Failed to decode request body: %v\", err)\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tif req.Vendor == \"\" || req.Code == \"\" {\n\t\t\tt.Error(\"Expected vendor and code fields\")\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tresp := conversionResponse{\n\t\t\tJobID:     \"test-job-123\",\n\t\t\tStatus:    \"processing\",\n\t\t\tMessage:   \"Conversion job queued for processing\",\n\t\t\tStatusURL: \"https://example.com/migrate/test-job-123/status\",\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(http.StatusAccepted)\n\t\tjson.NewEncoder(w).Encode(resp)\n\t}))\n\tdefer server.Close()\n\n\treq := conversionRequest{\n\t\tVendor: \"github\",\n\t\tCode:   \"name: Test\\non: [push]\",\n\t}\n\n\tresp, err := submitConversionJobAtEndpoint(server.URL, req)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to submit Conversion job: %v\", err)\n\t}\n\n\tif resp.JobID != \"test-job-123\" {\n\t\tt.Errorf(\"Expected job ID 'test-job-123', got %q\", resp.JobID)\n\t}\n\n\tif resp.Status != \"processing\" {\n\t\tt.Errorf(\"Expected status 'processing', got %q\", resp.Status)\n\t}\n}\n\nfunc TestPollJobStatus(t *testing.T) {\n\tt.Parallel()\n\n\tattempt := 0\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tattempt++\n\n\t\tvar status string\n\t\tvar result string\n\n\t\t// First attempt returns \"processing\", second returns \"completed\"\n\t\tif attempt == 1 {\n\t\t\tstatus = \"processing\"\n\t\t} else {\n\t\t\tstatus = \"completed\"\n\t\t\tresult = \"steps:\\n  - command: echo 'test'\\n\"\n\t\t}\n\n\t\tresp := statusResponse{\n\t\t\tJobID:       \"test-job-123\",\n\t\t\tStatus:      status,\n\t\t\tVendor:      \"github\",\n\t\t\tCreatedAt:   time.Now().Format(time.RFC3339),\n\t\t\tCompletedAt: time.Now().Format(time.RFC3339),\n\t\t\tResult:      result,\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tjson.NewEncoder(w).Encode(resp)\n\t}))\n\tdefer server.Close()\n\n\tresult, err := pollJobStatusWithConfig(\"test-job-123\", pollConfig{\n\t\tendpoint:    server.URL,\n\t\tclient:      server.Client(),\n\t\tmaxAttempts: 2,\n\t\tinterval:    0,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to poll job status: %v\", err)\n\t}\n\n\tif result.Status != \"completed\" {\n\t\tt.Errorf(\"Expected status 'completed', got %q\", result.Status)\n\t}\n\n\tif result.Result == \"\" {\n\t\tt.Error(\"Expected result to be populated\")\n\t}\n}\n\nfunc TestPollJobStatusTimeout(t *testing.T) {\n\tt.Parallel()\n\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tresp := statusResponse{\n\t\t\tJobID:     \"test-job-123\",\n\t\t\tStatus:    \"processing\",\n\t\t\tVendor:    \"github\",\n\t\t\tCreatedAt: time.Now().Format(time.RFC3339),\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tjson.NewEncoder(w).Encode(resp)\n\t}))\n\tdefer server.Close()\n\n\tresult, err := pollJobStatusWithConfig(\"test-job-123\", pollConfig{\n\t\tendpoint:    server.URL,\n\t\tclient:      server.Client(),\n\t\tmaxAttempts: 1,\n\t\tinterval:    0,\n\t})\n\tif err == nil {\n\t\tt.Error(\"Expected timeout error but got none\")\n\t\treturn\n\t}\n\n\tif result != nil {\n\t\tt.Error(\"Expected nil result on timeout\")\n\t}\n\n\tif !strings.Contains(err.Error(), \"timed out\") {\n\t\tt.Errorf(\"Expected timeout error, got: %v\", err)\n\t}\n}\n\nfunc TestMigrateCommandCreation(t *testing.T) {\n\tt.Parallel()\n\n\tcmd := &ConvertCmd{\n\t\tFile:    \"test.yml\",\n\t\tVendor:  \"github\",\n\t\tTimeout: 300,\n\t}\n\n\tif cmd.File != \"test.yml\" {\n\t\tt.Errorf(\"Expected File to be 'test.yml', got %q\", cmd.File)\n\t}\n\n\tif cmd.Vendor != \"github\" {\n\t\tt.Errorf(\"Expected Vendor to be 'github', got %q\", cmd.Vendor)\n\t}\n\n\tif cmd.Timeout != 300 {\n\t\tt.Errorf(\"Expected Timeout to be 300, got %d\", cmd.Timeout)\n\t}\n}\n\nfunc TestMigrateAutoDetection(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := t.TempDir()\n\ttestFile := filepath.Join(tmpDir, \".github\", \"workflows\", \"test.yml\")\n\n\tif err := os.MkdirAll(filepath.Dir(testFile), 0o755); err != nil {\n\t\tt.Fatalf(\"Failed to create test directory: %v\", err)\n\t}\n\n\ttestWorkflow := `name: Test\non: [push]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - run: echo \"Test\"\n`\n\tif err := os.WriteFile(testFile, []byte(testWorkflow), 0o644); err != nil {\n\t\tt.Fatalf(\"Failed to write test file: %v\", err)\n\t}\n\n\tvendor, err := detectVendor(testFile)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to detect vendor: %v\", err)\n\t}\n\n\tif vendor != \"github\" {\n\t\tt.Errorf(\"Expected vendor to be 'github', got %q\", vendor)\n\t}\n}\n"
  },
  {
    "path": "cmd/pipeline/copy.go",
    "content": "package pipeline\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype CopyCmd struct {\n\tPipeline         string `arg:\"\" help:\"Source pipeline to copy (slug or org/slug). Uses current pipeline if not specified.\" optional:\"\"`\n\tOrg              string `help:\"Organization slug\" name:\"org\"`\n\tTarget           string `help:\"Name for the new pipeline, or org/name to copy to a different organization\" short:\"t\"`\n\tClusterUUID      string `help:\"Cluster UUID for the new pipeline\" name:\"cluster-uuid\"`\n\tClusterName      string `help:\"Cluster name for the new pipeline (resolved to UUID)\" name:\"cluster-name\"`\n\tClusterShorthand string `short:\"c\" hidden:\"\" name:\"c\" help:\"\"`\n\tDryRun           bool   `help:\"Show what would be copied without creating the pipeline\"`\n\toutput.OutputFlags\n}\n\n// we store the target organization and pipeline name for a future go-buildkite call\ntype copyTarget struct {\n\tOrg  string\n\tName string\n}\n\nfunc (c *CopyCmd) orgSlug(f *factory.Factory) string {\n\tif c.Org != \"\" {\n\t\treturn c.Org\n\t}\n\n\treturn f.Config.OrganizationSlug()\n}\n\nfunc (c *CopyCmd) Validate() error {\n\tif c.ClusterShorthand != \"\" {\n\t\treturn fmt.Errorf(\"-c is no longer supported for cluster; use --cluster-uuid or --cluster-name instead\")\n\t}\n\tif c.ClusterUUID != \"\" && c.ClusterName != \"\" {\n\t\treturn fmt.Errorf(\"only one of --cluster-uuid or --cluster-name can be specified\")\n\t}\n\treturn nil\n}\n\nfunc (c *CopyCmd) Help() string {\n\treturn `Copy an existing pipeline's configuration to create a new pipeline.\n\nThis command copies all configuration from a source pipeline including:\n- Pipeline steps (YAML configuration)\n- Repository settings\n- Branch configuration\n- Build skipping/cancellation rules\n- Provider settings (trigger mode, PR builds, commit statuses, etc.)\n- Environment variables\n- Tags and visibility\n\nWhen copying to a different organization, cluster configuration is skipped\n(clusters are organization-specific).\n\nExamples:\n  # Copy the current pipeline to a new pipeline\n  $ bk pipeline cp --target \"my-pipeline-v2\"\n\n  # Copy a specific pipeline\n  $ bk pipeline cp my-existing-pipeline --target \"my-new-pipeline\"\n\n  # Copy a pipeline from another org (if you have access)\n  $ bk pipeline cp other-org/their-pipeline --target \"my-copy\"\n\n  # Copy to a different organization\n  $ bk pipeline cp my-pipeline --target \"other-org/my-pipeline\" --cluster-uuid \"8302f0b-9b99-4663-23f3-2d64f88s693e\"\n\n  # Copy to a different organization using cluster name\n  $ bk pipeline cp my-pipeline --target \"other-org/my-pipeline\" --cluster-name \"my-cluster\"\n\n  # Interactive mode - prompts for source and target\n  $ bk pipeline cp\n\n  # Preview what would be copied without creating\n  $ bk pipeline cp my-pipeline --target \"copy\" --dry-run\n\n  # Output the new pipeline details as JSON\n  $ bk pipeline cp my-pipeline -t \"new-pipeline\" -o json\n`\n}\n\nfunc (c *CopyCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()), factory.WithOrgOverride(c.Org))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfigurationForOrg(f.Config, kongCtx.Command(), c.Org); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\t// Resolve source pipeline\n\t// looks at current project if no source provided, or tries to resolve it using the current selected org\n\tsourcePipeline, err := c.resolveSourcePipeline(ctx, f)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Get target org and name\n\t// Spoiler: we use `/` as an indicator for org/pipeline split\n\ttarget, err := c.resolveTarget(f, sourcePipeline.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsource, err := c.fetchSourcePipeline(ctx, f, sourcePipeline.Org, sourcePipeline.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Determine if this is a cross-org copy\n\tisCrossOrg := target.Org != sourcePipeline.Org\n\n\t// Resolve cluster ID - required for cross-org copies\n\tclusterID, err := c.resolveCluster(ctx, f, target.Org, source.ClusterID, isCrossOrg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.DryRun {\n\t\treturn c.runDryRun(kongCtx, f, source, target, isCrossOrg, clusterID)\n\t}\n\n\treturn c.runCopy(kongCtx, f, source, target, isCrossOrg, clusterID)\n}\n\nfunc (c *CopyCmd) resolveSourcePipeline(ctx context.Context, f *factory.Factory) (*pipeline.Pipeline, error) {\n\tvar args []string\n\tif c.Pipeline != \"\" {\n\t\targs = []string{c.Pipeline}\n\t}\n\n\tpicker := resolver.PickOneWithFactory(f)\n\tcachedPicker := resolver.CachedPicker(f.Config, picker)\n\trepositoryResolver := resolver.ResolveFromRepository(f, cachedPicker)\n\tif c.Org != \"\" {\n\t\trepositoryResolver = resolver.ResolveFromRepositoryInOrg(f, cachedPicker, c.Org)\n\t}\n\n\tpipelineRes := resolver.NewAggregateResolver(\n\t\tresolver.WithOrg(c.Org, resolver.ResolveFromPositionalArgument(args, 0, f.Config)),\n\t\tresolver.WithOrg(c.Org, resolver.ResolveFromConfig(f.Config, picker)),\n\t\trepositoryResolver,\n\t)\n\n\tp, err := pipelineRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not resolve source pipeline, ensure correct config is in use (`bk org ls`): %w\", err)\n\t}\n\n\treturn p, nil\n}\n\nfunc (c *CopyCmd) resolveTarget(f *factory.Factory, sourceName string) (*copyTarget, error) {\n\ttargetStr := c.Target\n\tif targetStr == \"\" {\n\t\t// Interactive prompt for target name\n\t\tdefaultName := fmt.Sprintf(\"%s-copy\", sourceName)\n\t\tvar err error\n\t\ttargetStr, err = bkIO.PromptForInput(\"Target pipeline (or org/pipeline)\", defaultName, f.NoInput)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Parse target - could be \"name\" or \"org/name\"\n\t// we check to see if `/` is present for org name, if not we use the existing org selected\n\treturn parseTarget(targetStr, c.orgSlug(f)), nil\n}\n\n// parseTarget parses a target string into org and name components.\n// If no org is specified, defaultOrg is used which is the current selected org.\nfunc parseTarget(target, defaultOrg string) *copyTarget {\n\tif strings.Contains(target, \"/\") {\n\t\tparts := strings.SplitN(target, \"/\", 2)\n\t\treturn &copyTarget{\n\t\t\tOrg:  parts[0],\n\t\t\tName: parts[1],\n\t\t}\n\t}\n\treturn &copyTarget{\n\t\tOrg:  defaultOrg,\n\t\tName: target,\n\t}\n}\n\nfunc (c *CopyCmd) resolveCluster(ctx context.Context, f *factory.Factory, targetOrg, sourceClusterID string, isCrossOrg bool) (string, error) {\n\tif c.ClusterUUID != \"\" {\n\t\treturn c.ClusterUUID, nil\n\t}\n\n\tif c.ClusterName != \"\" {\n\t\treturn resolveClusterName(ctx, f, targetOrg, c.ClusterName)\n\t}\n\n\tif !isCrossOrg {\n\t\treturn sourceClusterID, nil\n\t}\n\n\treturn bkIO.PromptForInput(\"Target cluster UUID (required for cross-org copy)\", \"\", f.NoInput)\n}\n\nfunc (c *CopyCmd) fetchSourcePipeline(ctx context.Context, f *factory.Factory, org, slug string) (*buildkite.Pipeline, error) {\n\tvar pipeline buildkite.Pipeline\n\tvar resp *buildkite.Response\n\tvar err error\n\n\tif err = bkIO.SpinWhile(f, fmt.Sprintf(\"Fetching pipeline %s/%s\", org, slug), func() error {\n\t\tpipeline, resp, err = f.RestAPIClient.Pipelines.Get(ctx, org, slug)\n\t\treturn err\n\t}); err != nil {\n\t\tif resp != nil && resp.StatusCode == http.StatusNotFound {\n\t\t\treturn nil, fmt.Errorf(\"pipeline %s/%s not found\", org, slug)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to fetch pipeline: %w\", err)\n\t}\n\n\treturn &pipeline, nil\n}\n\n// runDryRun allows a user to validate what their changes will do, based on the current `--dry-run` flag in Create\nfunc (c *CopyCmd) runDryRun(kongCtx *kong.Context, f *factory.Factory, source *buildkite.Pipeline, target *copyTarget, isCrossOrg bool, clusterID string) error {\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tcreateReq := c.buildCreatePipeline(source, target.Name, isCrossOrg, clusterID)\n\n\t// For dry-run, default to JSON if text format requested\n\tif format == output.FormatText {\n\t\tformat = output.FormatJSON\n\t}\n\n\treturn output.Write(kongCtx.Stdout, createReq, format)\n}\n\nfunc (c *CopyCmd) runCopy(kongCtx *kong.Context, f *factory.Factory, source *buildkite.Pipeline, target *copyTarget, isCrossOrg bool, clusterID string) error {\n\tctx := context.Background()\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\t// For cross-org copies, we need a client authenticated for the target org\n\ttargetClient := f.RestAPIClient\n\tif isCrossOrg {\n\t\tvar err error\n\t\ttargetClient, err = c.getClientForOrg(f, target.Org)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tcreateReq := c.buildCreatePipeline(source, target.Name, isCrossOrg, clusterID)\n\n\tvar newPipeline buildkite.Pipeline\n\tvar resp *buildkite.Response\n\tvar err error\n\n\tif err = bkIO.SpinWhile(f, fmt.Sprintf(\"Creating pipeline %s/%s\", target.Org, target.Name), func() error {\n\t\tnewPipeline, resp, err = targetClient.Pipelines.Create(ctx, target.Org, createReq)\n\t\treturn err\n\t}); err != nil {\n\t\tif resp != nil && resp.StatusCode == http.StatusUnprocessableEntity {\n\t\t\t// Check if a pipeline with this name already exists and error out if it does (not fussed with adding -1, -2 etc)\n\t\t\tif existing := c.findPipelineByName(ctx, targetClient, target); existing != nil {\n\t\t\t\treturn fmt.Errorf(\"a pipeline with the name '%s' already exists: %s\", target.Name, existing.WebURL)\n\t\t\t}\n\t\t}\n\t\treturn fmt.Errorf(\"failed to create pipeline: %w\", err)\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(kongCtx.Stdout, newPipeline, format)\n\t}\n\n\tfmt.Printf(\"%s\\n\", newPipeline.WebURL)\n\treturn nil\n}\n\n// getClientForOrg creates a Buildkite client authenticated for the specified organization\nfunc (c *CopyCmd) getClientForOrg(f *factory.Factory, org string) (*buildkite.Client, error) {\n\ttoken := f.Config.APITokenForOrg(org)\n\tif token == \"\" {\n\t\treturn nil, fmt.Errorf(\"no API token configured for organization %q. Run 'bk configure' to add it\", org)\n\t}\n\n\treturn buildkite.NewOpts(\n\t\tbuildkite.WithBaseURL(f.Config.RESTAPIEndpoint()),\n\t\tbuildkite.WithTokenAuth(token),\n\t)\n}\n\nfunc (c *CopyCmd) buildCreatePipeline(source *buildkite.Pipeline, targetName string, isCrossOrg bool, clusterID string) buildkite.CreatePipeline {\n\tcreate := buildkite.CreatePipeline{\n\t\tName:          targetName,\n\t\tRepository:    source.Repository,\n\t\tConfiguration: source.Configuration,\n\n\t\t// Branch and build settings\n\t\tDefaultBranch:                   source.DefaultBranch,\n\t\tDescription:                     source.Description,\n\t\tBranchConfiguration:             source.BranchConfiguration,\n\t\tSkipQueuedBranchBuilds:          source.SkipQueuedBranchBuilds,\n\t\tSkipQueuedBranchBuildsFilter:    source.SkipQueuedBranchBuildsFilter,\n\t\tCancelRunningBranchBuilds:       source.CancelRunningBranchBuilds,\n\t\tCancelRunningBranchBuildsFilter: source.CancelRunningBranchBuildsFilter,\n\n\t\t// Visibility and tags\n\t\tVisibility: source.Visibility,\n\t\tTags:       source.Tags,\n\n\t\t// Provider settings (trigger mode, PR builds, commit statuses, etc.)\n\t\tProviderSettings: source.Provider.Settings,\n\t}\n\n\t// Use explicit cluster if provided, otherwise copy from source for same-org copies\n\tif clusterID != \"\" {\n\t\tcreate.ClusterID = clusterID\n\t} else if !isCrossOrg {\n\t\tcreate.ClusterID = source.ClusterID\n\t}\n\n\t// Convert environment variables (map[string]any -> map[string]string)\n\tif len(source.Env) > 0 {\n\t\tcreate.Env = make(map[string]string, len(source.Env))\n\t\tfor k, v := range source.Env {\n\t\t\tcreate.Env[k] = fmt.Sprintf(\"%v\", v)\n\t\t}\n\t}\n\n\treturn create\n}\n\nfunc (c *CopyCmd) findPipelineByName(ctx context.Context, client *buildkite.Client, target *copyTarget) *buildkite.Pipeline {\n\topts := buildkite.PipelineListOptions{\n\t\tListOptions: buildkite.ListOptions{\n\t\t\tPerPage: 100,\n\t\t},\n\t}\n\n\tpipelines, _, err := client.Pipelines.List(ctx, target.Org, &opts)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tfor _, p := range pipelines {\n\t\tif p.Name == target.Name {\n\t\t\treturn &p\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/pipeline/create.go",
    "content": "package pipeline\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/graphql\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype CreateCmd struct {\n\tName             string `arg:\"\" help:\"Name of the pipeline\" required:\"\"`\n\tOrg              string `help:\"Organization slug.\" name:\"org\"`\n\tDescription      string `help:\"Description of the pipeline\" short:\"d\"`\n\tRepository       string `help:\"Repository URL\" short:\"r\"`\n\tClusterUUID      string `help:\"Cluster UUID to assign the pipeline to\" name:\"cluster-uuid\"`\n\tClusterName      string `help:\"Cluster name to assign the pipeline to (resolved to UUID)\" name:\"cluster-name\"`\n\tClusterShorthand string `short:\"c\" hidden:\"\" name:\"c\" help:\"\"`\n\tCreateWebhook    bool   `help:\"Create an SCM webhook for the pipeline (GitHub and GitHub Enterprise only)\" short:\"W\"`\n\tDryRun           bool   `help:\"Simulate pipeline creation without actually creating it\"`\n\toutput.OutputFlags\n}\n\nfunc (c *CreateCmd) orgSlug(conf *config.Config) string {\n\tif c.Org != \"\" {\n\t\treturn c.Org\n\t}\n\treturn conf.OrganizationSlug()\n}\n\nfunc (c *CreateCmd) Validate() error {\n\tif c.ClusterShorthand != \"\" {\n\t\treturn fmt.Errorf(\"-c is no longer supported for cluster; use --cluster-uuid or --cluster-name instead\")\n\t}\n\tif c.ClusterUUID != \"\" && c.ClusterName != \"\" {\n\t\treturn fmt.Errorf(\"only one of --cluster-uuid or --cluster-name can be specified\")\n\t}\n\treturn nil\n}\n\nfunc (c *CreateCmd) Help() string {\n\treturn `Creates a new pipeline in the current org and outputs the URL to the pipeline.\n\nYou can specify a --dry-run flag to see the pipeline that would be created without\nactually creating it. This outputs a JSON representation of the pipeline to be created by default.\n\nUse --cluster-uuid to assign a pipeline to a cluster by UUID, or --cluster-name to\nassign by name (the name will be resolved to the corresponding UUID).\n\nExamples:\n  # Create a new pipeline\n  $ bk pipeline create \"My Pipeline\" --description \"My pipeline description\" --repository \"git@github.com:org/repo.git\"\n\n  # Create a new pipeline and view the created pipeline in JSON format\n  $ bk pipeline create \"My Pipeline\" --description \"My pipeline description\" --repository \"git@github.com:org/repo.git\" --output json\n\n  # Create a pipeline with a cluster (by UUID)\n  $ bk pipeline create \"My Pipeline\" -d \"Description\" -r \"git@github.com:org/repo.git\" --cluster-uuid \"cluster-uuid-123\"\n\n  # Create a pipeline with a cluster (by name)\n  $ bk pipeline create \"My Pipeline\" -d \"Description\" -r \"git@github.com:org/repo.git\" --cluster-name \"my-cluster\"\n\n  # Create a pipeline and set up a GitHub webhook\n  $ bk pipeline create \"My Pipeline\" -d \"Description\" -r \"git@github.com:org/repo.git\" --create-webhook\n\n  # Simulate creating a pipeline and view the output in yaml format\n  $ bk pipeline create \"My Pipeline\" -d \"Description\" -r \"git@github.com:org/repo.git\" --dry-run --output yaml\n`\n}\n\nfunc (c *CreateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()), factory.WithOrgOverride(c.Org))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfigurationForOrg(f.Config, kongCtx.Command(), c.Org); err != nil {\n\t\treturn err\n\t}\n\n\tif c.DryRun {\n\t\treturn c.runPipelineCreateDryRun(kongCtx, f)\n\t}\n\treturn c.runPipelineCreate(kongCtx, f)\n}\n\nfunc (c *CreateCmd) runPipelineCreateDryRun(kongCtx *kong.Context, f *factory.Factory) error {\n\tctx := context.Background()\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tpipeline, err := c.createPipelineDryRun(ctx, f)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// for dry-run, if text format is requested, always default to json\n\tif format == output.FormatText {\n\t\tformat = output.FormatJSON\n\t}\n\treturn output.Write(kongCtx.Stdout, pipeline, format)\n}\n\nfunc (c *CreateCmd) runPipelineCreate(kongCtx *kong.Context, f *factory.Factory) error {\n\tctx := context.Background()\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tpipeline, err := c.createPipeline(ctx, f)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.CreateWebhook {\n\t\trepoURL := getRepositoryURL(f, c.Repository)\n\t\tif repoURL == \"\" {\n\t\t\tfmt.Fprintln(kongCtx.Stderr, \"Warning: could not determine repository URL, skipping webhook creation\")\n\t\t} else if !isGitHubURL(repoURL) {\n\t\t\tfmt.Fprintln(kongCtx.Stderr, \"Warning: webhook creation is only supported for GitHub repositories, skipping\")\n\t\t} else {\n\t\t\tif err := createWebhook(ctx, f, pipeline.GraphQLID); err != nil {\n\t\t\t\treturn fmt.Errorf(\"pipeline created but webhook creation failed: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(kongCtx.Stdout, pipeline, format)\n\t}\n\tfmt.Printf(\"%s\\n\", pipeline.WebURL)\n\treturn nil\n}\n\nfunc (c *CreateCmd) createPipeline(ctx context.Context, f *factory.Factory) (*buildkite.Pipeline, error) {\n\tclusterID, err := c.resolveClusterUUID(ctx, f)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trepoURL := getRepositoryURL(f, c.Repository)\n\n\tvar pipeline buildkite.Pipeline\n\tvar resp *buildkite.Response\n\n\tif err = bkIO.SpinWhile(f, fmt.Sprintf(\"Creating pipeline %s\", c.Name), func() error {\n\t\tcreatePipeline := buildkite.CreatePipeline{\n\t\t\tName:          c.Name,\n\t\t\tRepository:    repoURL,\n\t\t\tDescription:   c.Description,\n\t\t\tClusterID:     clusterID,\n\t\t\tConfiguration: \"steps:\\n  - label: \\\":pipeline:\\\"\\n    command: buildkite-agent pipeline upload\",\n\t\t}\n\n\t\tpipeline, resp, err = f.RestAPIClient.Pipelines.Create(ctx, c.orgSlug(f.Config), createPipeline)\n\t\treturn err\n\t}); err != nil {\n\t\t// Check if this is a 422 error (validation failed)\n\t\tif resp != nil && resp.StatusCode == http.StatusUnprocessableEntity {\n\t\t\t// Try to find an existing pipeline with the same name\n\t\t\tif existingPipeline := c.findPipelineByName(ctx, f); existingPipeline != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"a pipeline with the name '%s' already exists: %s\", c.Name, existingPipeline.WebURL)\n\t\t\t}\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn &pipeline, nil\n}\n\nfunc (c *CreateCmd) findPipelineByName(ctx context.Context, f *factory.Factory) *buildkite.Pipeline {\n\topts := buildkite.PipelineListOptions{\n\t\tListOptions: buildkite.ListOptions{\n\t\t\tPerPage: 100,\n\t\t},\n\t}\n\n\tpipelines, _, err := f.RestAPIClient.Pipelines.List(ctx, c.orgSlug(f.Config), &opts)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tfor _, p := range pipelines {\n\t\tif p.Name == c.Name {\n\t\t\treturn &p\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype PipelineDryRun struct {\n\tID                              string               `json:\"id\"`\n\tGraphQLID                       string               `json:\"graphql_id\"`\n\tURL                             string               `json:\"url\"`\n\tWebURL                          string               `json:\"web_url\"`\n\tName                            string               `json:\"name\"`\n\tDescription                     string               `json:\"description\"`\n\tSlug                            string               `json:\"slug\"`\n\tRepository                      string               `json:\"repository\"`\n\tClusterID                       string               `json:\"cluster_id\"`\n\tClusterURL                      string               `json:\"cluster_url\"`\n\tBranchConfiguration             string               `json:\"branch_configuration\"`\n\tDefaultBranch                   string               `json:\"default_branch\"`\n\tSkipQueuedBranchBuilds          bool                 `json:\"skip_queued_branch_builds\"`\n\tSkipQueuedBranchBuildsFilter    string               `json:\"skip_queued_branch_builds_filter\"`\n\tCancelRunningBranchBuilds       bool                 `json:\"cancel_running_branch_builds\"`\n\tCancelRunningBranchBuildsFilter string               `json:\"cancel_running_branch_builds_filter\"`\n\tBuildsURL                       string               `json:\"builds_url\"`\n\tBadgeURL                        string               `json:\"badge_url\"`\n\tCreatedAt                       *buildkite.Timestamp `json:\"created_at\"`\n\tEnv                             map[string]any       `json:\"env\"`\n\tScheduledBuildsCount            int                  `json:\"scheduled_builds_count\"`\n\tRunningBuildsCount              int                  `json:\"running_builds_count\"`\n\tScheduledJobsCount              int                  `json:\"scheduled_jobs_count\"`\n\tRunningJobsCount                int                  `json:\"running_jobs_count\"`\n\tWaitingJobsCount                int                  `json:\"waiting_jobs_count\"`\n\tVisibility                      string               `json:\"visibility\"`\n\tTags                            []string             `json:\"tags\"`\n\tConfiguration                   string               `json:\"configuration\"`\n\tSteps                           []buildkite.Step     `json:\"steps\"`\n\tProvider                        buildkite.Provider   `json:\"provider\"`\n\tPipelineTemplateUUID            string               `json:\"pipeline_template_uuid\"`\n\tAllowRebuilds                   bool                 `json:\"allow_rebuilds\"`\n\tEmoji                           *string              `json:\"emoji\"`\n\tColor                           *string              `json:\"color\"`\n\tCreatedBy                       *buildkite.User      `json:\"created_by\"`\n}\n\nfunc initialisePipelineDryRun() PipelineDryRun {\n\treturn PipelineDryRun{\n\t\tEnv:   nil,\n\t\tTags:  nil,\n\t\tSteps: []buildkite.Step{},\n\t\tProvider: buildkite.Provider{\n\t\t\tSettings: &buildkite.GitHubSettings{},\n\t\t},\n\t\tAllowRebuilds: true,\n\t}\n}\n\nfunc (c *CreateCmd) createPipelineDryRun(ctx context.Context, f *factory.Factory) (*PipelineDryRun, error) {\n\tpipelineSlug := generateSlug(c.Name)\n\n\tpipelineSlug, err := getAvailablePipelineSlug(ctx, f, c.orgSlug(f.Config), pipelineSlug, c.Name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\torgSlug := c.orgSlug(f.Config)\n\tpipeline := initialisePipelineDryRun()\n\n\tpipeline.ID = \"00000000-0000-0000-0000-000000000000\"\n\tpipeline.GraphQLID = \"UGlwZWxpbmUtLS0wMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDA=\"\n\tpipeline.URL = fmt.Sprintf(\"https://api.buildkite.com/v2/organizations/%s/pipelines/%s\", orgSlug, pipelineSlug)\n\tpipeline.WebURL = fmt.Sprintf(\"https://buildkite.com/%s/%s\", orgSlug, pipelineSlug)\n\tpipeline.Name = c.Name\n\tpipeline.Description = c.Description\n\tpipeline.Slug = pipelineSlug\n\tpipeline.Repository = c.Repository\n\tclusterUUID, _ := c.resolveClusterUUID(ctx, f)\n\tpipeline.ClusterID = clusterUUID\n\tpipeline.ClusterURL = getClusterUrl(orgSlug, clusterUUID)\n\tpipeline.DefaultBranch = \"main\"\n\tpipeline.BuildsURL = fmt.Sprintf(\"https://api.buildkite.com/v2/organizations/%s/pipelines/%s/builds\", orgSlug, pipelineSlug)\n\tpipeline.BadgeURL = fmt.Sprintf(\"https://badge.buildkite.com/%s.svg\", \"00000000000000000000000000000000000000000000000000\")\n\tpipeline.CreatedAt = buildkite.NewTimestamp(time.Now())\n\tpipeline.Visibility = \"private\"\n\tpipeline.Configuration = \"steps:\\n  - label: \\\":pipeline:\\\"\\n    command: buildkite-agent pipeline upload\"\n\tpipeline.Steps = []buildkite.Step{\n\t\t{\n\t\t\tType:    \":pipeline:\",\n\t\t\tName:    \":pipeline:\",\n\t\t\tCommand: \"buildkite-agent pipeline upload\",\n\t\t},\n\t}\n\tpipeline.Provider = buildkite.Provider{\n\t\tID:         \"github\",\n\t\tWebhookURL: \"https://webhook.buildkite.com/deliver/00000000000000000000000000000000000000000000000000\",\n\t\tSettings: &buildkite.GitHubSettings{\n\t\t\tTriggerMode:         \"code\",\n\t\t\tBuildPullRequests:   true,\n\t\t\tBuildBranches:       true,\n\t\t\tPublishCommitStatus: true,\n\t\t\tRepository:          extractRepoPath(c.Repository),\n\t\t},\n\t}\n\n\tpipeline.CreatedBy = getCreatedByDetails(ctx, f)\n\n\treturn &pipeline, nil\n}\n\nfunc generateSlug(name string) string {\n\tname = strings.TrimSpace(name)\n\n\tvar slug strings.Builder\n\tlastWasSeparator := false\n\n\tfor _, c := range strings.ToLower(name) {\n\t\tif (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') {\n\t\t\tslug.WriteRune(c)\n\t\t\tlastWasSeparator = false\n\t\t} else if c == ' ' || c == '-' || c == '_' {\n\t\t\tif !lastWasSeparator && slug.Len() > 0 {\n\t\t\t\tslug.WriteRune('-')\n\t\t\t\tlastWasSeparator = true\n\t\t\t}\n\t\t}\n\t}\n\n\tresult := slug.String()\n\treturn strings.TrimRight(result, \"-\")\n}\n\nfunc extractRepoPath(repoURL string) string {\n\tif strings.HasPrefix(repoURL, \"git@github.com:\") {\n\t\tpath := strings.TrimPrefix(repoURL, \"git@github.com:\")\n\t\treturn strings.TrimSuffix(path, \".git\")\n\t}\n\n\tif strings.HasPrefix(repoURL, \"https://github.com/\") {\n\t\tpath := strings.TrimPrefix(repoURL, \"https://github.com/\")\n\t\treturn strings.TrimSuffix(path, \".git\")\n\t}\n\n\treturn repoURL\n}\n\nfunc getAvailablePipelineSlug(ctx context.Context, f *factory.Factory, org, pipelineSlug, pipelineName string) (string, error) {\n\tpipeline, resp, err := f.RestAPIClient.Pipelines.Get(ctx, org, pipelineSlug)\n\tif err != nil {\n\t\tif resp != nil && resp.StatusCode == 404 {\n\t\t\treturn pipelineSlug, nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"failed to validate pipeline name\")\n\t}\n\n\tif pipeline.Name == pipelineName {\n\t\treturn \"\", fmt.Errorf(\"a pipeline with the name '%s' already exists\", pipelineName)\n\t}\n\n\tcounter := 1\n\tfor {\n\t\tnewSlug := fmt.Sprintf(\"%s-%d\", pipelineSlug, counter)\n\t\tpipeline, resp, err := f.RestAPIClient.Pipelines.Get(ctx, org, newSlug)\n\t\tif err != nil {\n\t\t\tif resp != nil && resp.StatusCode == 404 {\n\t\t\t\treturn newSlug, nil\n\t\t\t}\n\t\t\treturn \"\", fmt.Errorf(\"failed to validate pipeline name\")\n\t\t}\n\n\t\tif pipeline.Name == pipelineName {\n\t\t\treturn \"\", fmt.Errorf(\"a pipeline with the name '%s' already exists\", pipelineName)\n\t\t}\n\n\t\tcounter++\n\t\tif counter > 1000 {\n\t\t\treturn \"\", fmt.Errorf(\"unable to find available slug after 1000 attempts\")\n\t\t}\n\t}\n}\n\nfunc getClusterUrl(orgSlug, clusterID string) string {\n\tif clusterID == \"\" {\n\t\treturn \"\"\n\t}\n\treturn fmt.Sprintf(\"https://api.buildkite.com/v2/organizations/%s/clusters/%s\", orgSlug, clusterID)\n}\n\nfunc getClusters(ctx context.Context, f *factory.Factory, org string) (map[string]string, error) {\n\tclusterMap := make(map[string]string)\n\tpage := 1\n\tper_page := 30\n\n\tfor more_clusters := true; more_clusters; {\n\t\topts := buildkite.ClustersListOptions{\n\t\t\tListOptions: buildkite.ListOptions{\n\t\t\t\tPage:    page,\n\t\t\t\tPerPage: per_page,\n\t\t\t},\n\t\t}\n\t\tclusters, resp, err := f.RestAPIClient.Clusters.List(ctx, org, &opts)\n\t\tif err != nil {\n\t\t\treturn map[string]string{}, err\n\t\t}\n\n\t\tif len(clusters) < 1 {\n\t\t\treturn map[string]string{}, nil\n\t\t}\n\n\t\tfor _, c := range clusters {\n\t\t\tclusterMap[c.Name] = c.ID\n\t\t}\n\n\t\tif resp.NextPage == 0 {\n\t\t\tmore_clusters = false\n\t\t} else {\n\t\t\tpage = resp.NextPage\n\t\t}\n\t}\n\treturn clusterMap, nil\n}\n\nfunc listClusterNames(ctx context.Context, f *factory.Factory, org string) ([]string, error) {\n\tclusterMap, err := getClusters(ctx, f, org)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclusterNames := make([]string, 0, len(clusterMap))\n\tfor name := range clusterMap {\n\t\tclusterNames = append(clusterNames, name)\n\t}\n\tsort.Strings(clusterNames)\n\n\treturn clusterNames, nil\n}\n\nfunc (c *CreateCmd) resolveClusterUUID(ctx context.Context, f *factory.Factory) (string, error) {\n\tif c.ClusterUUID != \"\" {\n\t\treturn c.ClusterUUID, nil\n\t}\n\tif c.ClusterName == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\treturn resolveClusterName(ctx, f, c.orgSlug(f.Config), c.ClusterName)\n}\n\nfunc resolveClusterName(ctx context.Context, f *factory.Factory, org, clusterName string) (string, error) {\n\tclusterMap, err := getClusters(ctx, f, org)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch clusters: %w\", err)\n\t}\n\n\tif clusterID, exists := clusterMap[clusterName]; exists {\n\t\treturn clusterID, nil\n\t}\n\n\tclusterNames, _ := listClusterNames(ctx, f, org)\n\tif len(clusterNames) > 0 {\n\t\treturn \"\", fmt.Errorf(\"cluster '%s' not found. Available clusters: %s\", clusterName, strings.Join(clusterNames, \", \"))\n\t}\n\n\treturn \"\", fmt.Errorf(\"cluster '%s' not found\", clusterName)\n}\n\nfunc getCreatedByDetails(ctx context.Context, f *factory.Factory) *buildkite.User {\n\tuser, _, err := f.RestAPIClient.User.CurrentUser(ctx)\n\tif err != nil {\n\t\treturn nil\n\t}\n\treturn &user\n}\n\n// isGitHubURL checks if a repository URL points to a GitHub-hosted repository.\nfunc isGitHubURL(repoURL string) bool {\n\treturn strings.HasPrefix(repoURL, \"git@github.com:\") ||\n\t\tstrings.Contains(repoURL, \"://github.com/\") ||\n\t\tstrings.HasPrefix(repoURL, \"git@github.\") ||\n\t\tstrings.Contains(repoURL, \"://github.\")\n}\n\n// getRepositoryURL determines the repository URL from the flag or the git remote.\nfunc getRepositoryURL(f *factory.Factory, repoFlag string) string {\n\tif repoFlag != \"\" {\n\t\treturn repoFlag\n\t}\n\n\tif f == nil || f.GitRepository == nil {\n\t\treturn \"\"\n\t}\n\n\tc, err := f.GitRepository.Config()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\torigin, ok := c.Remotes[\"origin\"]\n\tif !ok || len(origin.URLs) == 0 {\n\t\treturn \"\"\n\t}\n\n\treturn origin.URLs[0]\n}\n\nfunc createWebhook(ctx context.Context, f *factory.Factory, pipelineGraphQLID string) error {\n\treturn bkIO.SpinWhile(f, \"Creating webhook\", func() error {\n\t\t_, err := graphql.PipelineCreateWebhook(ctx, f.GraphQLClient, pipelineGraphQLID)\n\t\treturn err\n\t})\n}\n"
  },
  {
    "path": "cmd/pipeline/create_test.go",
    "content": "package pipeline\n\nimport (\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\nfunc TestIsGitHubURL(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\turl      string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"GitHub SSH URL\",\n\t\t\turl:      \"git@github.com:org/repo.git\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"GitHub HTTPS URL\",\n\t\t\turl:      \"https://github.com/org/repo.git\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"GitHub HTTPS URL without .git\",\n\t\t\turl:      \"https://github.com/org/repo\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"GitHub Enterprise SSH URL\",\n\t\t\turl:      \"git@github.mycompany.com:org/repo.git\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"GitHub Enterprise HTTPS URL\",\n\t\t\turl:      \"https://github.mycompany.com/org/repo.git\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"GitLab SSH URL\",\n\t\t\turl:      \"git@gitlab.com:org/repo.git\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"GitLab HTTPS URL\",\n\t\t\turl:      \"https://gitlab.com/org/repo.git\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"Bitbucket SSH URL\",\n\t\t\turl:      \"git@bitbucket.org:org/repo.git\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"Bitbucket HTTPS URL\",\n\t\t\turl:      \"https://bitbucket.org/org/repo.git\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty string\",\n\t\t\turl:      \"\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := isGitHubURL(tt.url)\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"isGitHubURL(%q) = %v, want %v\", tt.url, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetRepositoryURL(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"returns flag value when provided\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tf := &factory.Factory{}\n\t\trepoURL := getRepositoryURL(f, \"git@github.com:org/repo.git\")\n\t\tif repoURL != \"git@github.com:org/repo.git\" {\n\t\t\tt.Errorf(\"expected flag value, got %q\", repoURL)\n\t\t}\n\t})\n\n\tt.Run(\"returns empty when no flag and nil git repo\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tf := &factory.Factory{}\n\t\trepoURL := getRepositoryURL(f, \"\")\n\t\tif repoURL != \"\" {\n\t\t\tt.Errorf(\"expected empty string, got %q\", repoURL)\n\t\t}\n\t})\n\n\tt.Run(\"returns empty when factory is nil and no flag\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\trepoURL := getRepositoryURL(nil, \"\")\n\t\tif repoURL != \"\" {\n\t\t\tt.Errorf(\"expected empty string, got %q\", repoURL)\n\t\t}\n\t})\n\n\tt.Run(\"returns flag value even when factory is nil\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\trepoURL := getRepositoryURL(nil, \"git@github.com:org/repo.git\")\n\t\tif repoURL != \"git@github.com:org/repo.git\" {\n\t\t\tt.Errorf(\"expected flag value, got %q\", repoURL)\n\t\t}\n\t})\n}\n\nfunc TestExtractRepoPath(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{name: \"GitHub SSH\", input: \"git@github.com:org/repo.git\", expected: \"org/repo\"},\n\t\t{name: \"GitHub HTTPS\", input: \"https://github.com/org/repo.git\", expected: \"org/repo\"},\n\t\t{name: \"GitHub HTTPS no .git\", input: \"https://github.com/org/repo\", expected: \"org/repo\"},\n\t\t{name: \"non-GitHub URL\", input: \"git@gitlab.com:org/repo.git\", expected: \"git@gitlab.com:org/repo.git\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := extractRepoPath(tt.input)\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"extractRepoPath(%q) = %q, want %q\", tt.input, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/pipeline/graphql/create_webhook.graphql",
    "content": "mutation PipelineCreateWebhook($id: ID!) {\n  pipelineCreateWebhook(input: { id: $id }) {\n    clientMutationId\n    pipelineID\n  }\n}\n"
  },
  {
    "path": "cmd/pipeline/list.go",
    "content": "package pipeline\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nconst (\n\tmaxPipelineLimit = 3000\n\tpageSize         = 100\n)\n\ntype ListCmd struct {\n\tOrg        string `help:\"Organization slug.\" name:\"org\"`\n\tName       string `help:\"Filter pipelines by name (supports partial matches, case insensitive)\" short:\"n\"`\n\tRepository string `help:\"Filter pipelines by repository URL (supports partial matches, case insensitive)\" short:\"r\"`\n\tLimit      int    `help:\"Maximum number of pipelines to return (max: 3000)\" short:\"l\" default:\"100\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ListCmd) Help() string {\n\treturn `List pipelines with optional filtering.\n\nThis command lists all pipelines in the current organization that match\nthe specified criteria. You can filter by pipeline name or repository URL.\n\nExamples:\n  # List all pipelines (default limit: 100)\n  $ bk pipeline list\n\n  # List pipelines matching a name pattern\n  $ bk pipeline list --name pipeline\n\n  # List pipelines by repository\n  $ bk pipeline list --repo my-repo\n\n  # Get more pipelines (automatically paginates)\n  $ bk pipeline list --limit 500\n\n  # Output as JSON\n  $ bk pipeline list --name pipeline -o json\n\n  # Use with other commands (e.g., get longest builds from matching pipelines)\n  $ bk pipeline list --name pipeline | xargs -I {} bk build list --pipeline {} --since 48h --duration 1h\n`\n}\n\nfunc (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()), factory.WithOrgOverride(c.Org))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfigurationForOrg(f.Config, kongCtx.Command(), c.Org); err != nil {\n\t\treturn err\n\t}\n\n\tif c.Limit > maxPipelineLimit {\n\t\treturn fmt.Errorf(\"limit cannot exceed %d pipelines (requested: %d)\", maxPipelineLimit, c.Limit)\n\t}\n\n\tctx := context.Background()\n\treturn c.runPipelineList(ctx, f)\n}\n\nfunc (c *ListCmd) runPipelineList(ctx context.Context, f *factory.Factory) error {\n\torg := c.Org\n\tif org == \"\" {\n\t\torg = f.Config.OrganizationSlug()\n\t}\n\tif org == \"\" {\n\t\treturn fmt.Errorf(\"no organization configured. Use 'bk configure' to set up your organization\")\n\t}\n\n\tlistOpts := c.pipelineListOptionsFromFlags()\n\n\tvar pipelines []buildkite.Pipeline\n\n\tif err := bkIO.SpinWhile(f, \"Loading pipelines\", func() error {\n\t\tvar apiErr error\n\t\tpipelines, apiErr = c.fetchPipelines(ctx, f, org, listOpts)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"failed to list pipelines: %w\", err)\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\tif len(pipelines) == 0 {\n\t\treturn output.WriteTextOrStructured(os.Stdout, format, []buildkite.Pipeline{}, \"No pipelines found matching the specified criteria.\")\n\t}\n\n\treturn c.displayPipelines(pipelines, f)\n}\n\nfunc (c *ListCmd) pipelineListOptionsFromFlags() *buildkite.PipelineListOptions {\n\tlistOpts := &buildkite.PipelineListOptions{\n\t\tListOptions: buildkite.ListOptions{\n\t\t\tPerPage: pageSize,\n\t\t},\n\t}\n\n\tif c.Name != \"\" {\n\t\tlistOpts.Name = c.Name\n\t}\n\tif c.Repository != \"\" {\n\t\tlistOpts.Repository = c.Repository\n\t}\n\n\treturn listOpts\n}\n\nfunc (c *ListCmd) fetchPipelines(ctx context.Context, f *factory.Factory, org string, listOpts *buildkite.PipelineListOptions) ([]buildkite.Pipeline, error) {\n\tvar allPipelines []buildkite.Pipeline\n\n\tfor page := 1; len(allPipelines) < c.Limit; page++ {\n\t\tlistOpts.Page = page\n\t\tlistOpts.PerPage = min(pageSize, c.Limit-len(allPipelines))\n\n\t\tpipelines, _, err := f.RestAPIClient.Pipelines.List(ctx, org, listOpts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif len(pipelines) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tallPipelines = append(allPipelines, pipelines...)\n\n\t\tif len(pipelines) < listOpts.PerPage {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn allPipelines, nil\n}\n\nfunc (c *ListCmd) displayPipelines(pipelines []buildkite.Pipeline, f *factory.Factory) error {\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, pipelines, format)\n\t}\n\n\trows := make([][]string, 0, len(pipelines))\n\tfor _, pipeline := range pipelines {\n\t\trows = append(rows, []string{\n\t\t\toutput.ValueOrDash(strings.TrimSpace(pipeline.Name)),\n\t\t\toutput.ValueOrDash(strings.TrimSpace(pipeline.Repository)),\n\t\t})\n\t}\n\n\ttable := output.Table(\n\t\t[]string{\"Name\", \"Repository\"},\n\t\trows,\n\t\tmap[string]string{\"name\": \"bold\", \"repository\": \"italic\"},\n\t)\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\t_, err := fmt.Fprintf(writer, \"Pipelines (%d)\\n\\n%s\\n\", len(pipelines), table)\n\treturn err\n}\n"
  },
  {
    "path": "cmd/pipeline/validate.go",
    "content": "package pipeline\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/xeipuuv/gojsonschema\"\n)\n\nconst schemaURL = \"https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json\"\n\n// fallbackSchema is a simplified schema used when the online schema cannot be accessed\n// It implements the basic structure validation but doesn't include all checks\nvar fallbackSchema = []byte(`{\n\t\"$schema\": \"http://json-schema.org/draft-07/schema#\",\n\t\"type\": \"object\",\n\t\"properties\": {\n\t\t\"steps\": {\n\t\t\t\"type\": \"array\",\n\t\t\t\"items\": {\n\t\t\t\t\"type\": \"object\",\n\t\t\t\t\"properties\": {\n\t\t\t\t\t\"label\": { \"type\": \"string\" },\n\t\t\t\t\t\"command\": { \"type\": \"string\" },\n\t\t\t\t\t\"plugins\": { \"type\": \"object\" },\n\t\t\t\t\t\"agents\": { \"type\": \"object\" },\n\t\t\t\t\t\"env\": { \"type\": \"object\" },\n\t\t\t\t\t\"branches\": { \"type\": [\"string\", \"object\"] },\n\t\t\t\t\t\"if\": { \"type\": \"string\" },\n\t\t\t\t\t\"depends_on\": { \"type\": [\"string\", \"array\"] }\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"env\": { \"type\": \"object\" },\n\t\t\"agents\": { \"type\": \"object\" }\n\t},\n\t\"required\": [\"steps\"]\n}`)\n\ntype ValidateCmd struct {\n\tFile []string `help:\"Path to the pipeline YAML file(s) to validate\" short:\"f\"`\n}\n\nfunc (c *ValidateCmd) Help() string {\n\treturn `Validate a pipeline YAML file against the Buildkite pipeline schema.\n\nBy default, this command looks for a file at .buildkite/pipeline.yaml or .buildkite/pipeline.yml\nin the current directory. You can specify different files using the --file flag.\n\nNote: This command does not require an API token since validation is done locally.\n\nExamples:\n  # Validate the default pipeline file\n  $ bk pipeline validate\n\n  # Validate a specific pipeline file\n  $ bk pipeline validate --file path/to/pipeline.yaml\n\n  # Validate multiple pipeline files\n  $ bk pipeline validate --file path/to/pipeline1.yaml --file path/to/pipeline2.yaml\n`\n}\n\nfunc (c *ValidateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tfilePaths := c.File\n\tif len(filePaths) == 0 {\n\t\tdefaultPath, err := findPipelineFile()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfilePaths = []string{defaultPath}\n\t}\n\n\tvar validationErrors []string\n\tfileCount := len(filePaths)\n\n\tfmt.Printf(\"Validating %d pipeline file(s)...\\n\\n\", fileCount)\n\n\tfor _, filePath := range filePaths {\n\t\terr := validatePipeline(os.Stdout, filePath)\n\t\tif err != nil {\n\t\t\tvalidationErrors = append(validationErrors, fmt.Sprintf(\"%s: %v\", filePath, err))\n\t\t}\n\t}\n\n\tif len(validationErrors) > 0 {\n\t\terrorCount := len(validationErrors)\n\t\tfmt.Printf(\"\\n%d of %d file(s) failed validation.\\n\", errorCount, fileCount)\n\t\treturn fmt.Errorf(\"pipeline validation failed\")\n\t}\n\n\tfmt.Println(\"\\nAll pipeline files passed validation successfully!\")\n\treturn nil\n}\n\n// findPipelineFile attempts to locate a pipeline file in the default locations\nfunc findPipelineFile() (string, error) {\n\t// Check for pipeline files in various standard locations\n\t// The order matches the Buildkite agent's lookup order\n\tpaths := []string{\n\t\t\"buildkite.yml\",\n\t\t\"buildkite.yaml\",\n\t\t\"buildkite.json\",\n\t\tfilepath.Join(\".buildkite\", \"pipeline.yml\"),\n\t\tfilepath.Join(\".buildkite\", \"pipeline.yaml\"),\n\t\tfilepath.Join(\".buildkite\", \"pipeline.json\"),\n\t\tfilepath.Join(\"buildkite\", \"pipeline.yml\"),\n\t\tfilepath.Join(\"buildkite\", \"pipeline.yaml\"),\n\t\tfilepath.Join(\"buildkite\", \"pipeline.json\"),\n\t}\n\n\t// Check each path\n\tfor _, path := range paths {\n\t\tif fileExists(path) {\n\t\t\treturn path, nil\n\t\t}\n\t}\n\n\t// If no file found, provide detailed error message\n\treturn \"\", fmt.Errorf(\"could not find pipeline file in default locations. Please specify a file with --file or create one in a standard location:\\n\" +\n\t\t\"  • .buildkite/pipeline.yml\\n\" +\n\t\t\"  • .buildkite/pipeline.yaml\\n\" +\n\t\t\"  • buildkite.yml\\n\" +\n\t\t\"  • buildkite.yaml\")\n}\n\n// fileExists checks if a file exists and is not a directory\nfunc fileExists(path string) bool {\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn !info.IsDir()\n}\n\n// validatePipeline validates the given pipeline file against the schema\nfunc validatePipeline(w io.Writer, filePath string) error {\n\t// Read the pipeline file\n\tpipelineData, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading pipeline file: %w\", err)\n\t}\n\n\t// Trim whitespace to handle empty files more gracefully\n\tif len(strings.TrimSpace(string(pipelineData))) == 0 {\n\t\tfmt.Fprintf(w, \"❌ Pipeline file is invalid: %s\\n\\n\", filePath)\n\t\tfmt.Fprintf(w, \"- File is empty\\n\")\n\t\treturn fmt.Errorf(\"empty pipeline file\")\n\t}\n\n\t// Convert YAML to JSON for validation\n\tjsonData, err := yaml.YAMLToJSON(pipelineData)\n\tif err != nil {\n\t\tfmt.Fprintf(w, \"❌ Pipeline file is invalid: %s\\n\\n\", filePath)\n\t\tfmt.Fprintf(w, \"- YAML parsing error: %s\\n\", err.Error())\n\t\tfmt.Fprintf(w, \"  Hint: Check for syntax errors like improper indentation, missing quotes, or invalid characters.\\n\")\n\t\treturn fmt.Errorf(\"invalid YAML format: %w\", err)\n\t}\n\n\t// Load the schema and document\n\tschemaLoader := gojsonschema.NewReferenceLoader(schemaURL)\n\tdocumentLoader := gojsonschema.NewBytesLoader(jsonData)\n\n\t// Try to validate against the online schema\n\tresult, err := gojsonschema.Validate(schemaLoader, documentLoader)\n\tif err != nil {\n\t\t// If online schema access fails, try the fallback schema\n\t\tfmt.Fprintf(w, \"⚠️  Warning: Could not access online pipeline schema: %s\\n\", err.Error())\n\t\tfmt.Fprintf(w, \"   Using simplified fallback schema for basic validation.\\n\\n\")\n\n\t\t// Create a schema loader using the fallback schema\n\t\tfallbackLoader := gojsonschema.NewBytesLoader(fallbackSchema)\n\t\tresult, err = gojsonschema.Validate(fallbackLoader, documentLoader)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(w, \"❌ Pipeline file is invalid: %s\\n\\n\", filePath)\n\t\t\tfmt.Fprintf(w, \"- Schema validation error: %s\\n\", err.Error())\n\t\t\treturn fmt.Errorf(\"schema validation error: %w\", err)\n\t\t}\n\t}\n\n\tif result.Valid() {\n\t\tfmt.Fprintf(w, \"✅ Pipeline file is valid: %s\\n\", filePath)\n\t\treturn nil\n\t}\n\n\t// Return validation errors\n\tfmt.Fprintf(w, \"❌ Pipeline file is invalid: %s\\n\\n\", filePath)\n\tfor _, err := range result.Errors() {\n\t\t// Format the error message for better readability\n\t\tmessage := formatValidationError(err)\n\t\tfmt.Fprintf(w, \"- %s\\n\", message)\n\t}\n\n\treturn fmt.Errorf(\"pipeline validation failed\")\n}\n\n// formatValidationError formats a validation error for better readability\nfunc formatValidationError(err gojsonschema.ResultError) string {\n\tfield := err.Field()\n\n\tif strings.Contains(field, \"[\") && strings.Contains(field, \"]\") {\n\t\tparts := strings.Split(field, \".\")\n\t\tfor i, part := range parts {\n\t\t\tif strings.Contains(part, \"[\") {\n\t\t\t\tindex := strings.TrimRight(strings.TrimLeft(part, \"[\"), \"]\")\n\t\t\t\tname := strings.Split(part, \"[\")[0]\n\t\t\t\tparts[i] = fmt.Sprintf(\"%s item #%s\", name, index)\n\t\t\t}\n\t\t}\n\t\tfield = strings.Join(parts, \" > \")\n\t} else if field != \"\" {\n\t\tfield = strings.ReplaceAll(field, \".\", \" > \")\n\t}\n\n\tmessage := err.Description()\n\n\t// Format the message with the field highlighted\n\tif field != \"\" {\n\t\tmessage = fmt.Sprintf(\"%s: %s\", field, message)\n\t}\n\n\t// Include details about what was received vs what was expected if available\n\tdetails := err.Details()\n\n\t// Add more context about expected values\n\tvar contextParts []string\n\tif val, ok := details[\"field\"]; ok && val != field {\n\t\tcontextParts = append(contextParts, fmt.Sprintf(\"field: %v\", val))\n\t}\n\tif val, ok := details[\"expected\"]; ok {\n\t\tcontextParts = append(contextParts, fmt.Sprintf(\"expected: %v\", val))\n\t}\n\tif val, ok := details[\"actual\"]; ok {\n\t\tcontextParts = append(contextParts, fmt.Sprintf(\"got: %v\", val))\n\t}\n\n\t// If we have context parts, add them to the message\n\tif len(contextParts) > 0 {\n\t\tmessage += fmt.Sprintf(\" (%s)\", strings.Join(contextParts, \", \"))\n\t}\n\n\t// Add a helpful hint based on the error type\n\tswitch err.Type() {\n\tcase \"required\":\n\t\tmessage += \"\\n    Hint: This field is required but was not found in your pipeline.\"\n\tcase \"type_error\":\n\t\tmessage += \"\\n    Hint: Check that you're using the correct data type for this field.\"\n\tcase \"enum\":\n\t\tmessage += \"\\n    Hint: The value must be one of the allowed options.\"\n\tcase \"const\":\n\t\tmessage += \"\\n    Hint: This field must have the specific required value.\"\n\tcase \"array_no_items\":\n\t\tmessage += \"\\n    Hint: This array cannot be empty.\"\n\t}\n\n\treturn message\n}\n"
  },
  {
    "path": "cmd/pipeline/validate_test.go",
    "content": "package pipeline\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/xeipuuv/gojsonschema\"\n)\n\nfunc TestValidatePipeline(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a test schema that matches actual Buildkite schema requirements:\n\t// This simplified schema ensures:\n\t// - Steps are required\n\t// - Each step must have at least a \"command\" field\n\t// - A \"label\" field is optional and must be a string\n\t// - A \"command\" field must be a string\n\ttestSchema := []byte(`{\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n        \"type\": \"object\",\n        \"required\": [\"steps\"],\n        \"properties\": {\n            \"steps\": {\n                \"type\": \"array\",\n                \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"label\": { \"type\": \"string\" },\n                        \"command\": { \"type\": \"string\" }\n                    },\n                    \"required\": [\"command\"]\n                }\n            }\n        }\n    }`)\n\n\ttestSchemaLoader := gojsonschema.NewBytesLoader(testSchema)\n\n\ttests := []struct {\n\t\tname         string\n\t\tfileContent  string\n\t\texpectError  bool\n\t\texpectOutput string\n\t}{\n\t\t{\n\t\t\tname: \"valid pipeline\",\n\t\t\tfileContent: `steps:\n  - label: \"Hello, world! 👋\"\n    command: echo \"Hello, world!\"`,\n\t\t\texpectError:  false,\n\t\t\texpectOutput: \"✅ Pipeline file is valid\",\n\t\t},\n\t\t{\n\t\t\tname: \"valid pipeline with command only\",\n\t\t\tfileContent: `steps:\n  - command: echo \"Hello, world!\"`,\n\t\t\texpectError:  false,\n\t\t\texpectOutput: \"✅ Pipeline file is valid\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid pipeline missing command\",\n\t\t\tfileContent: `steps:\n  - label: \"Hello, world!\"`,\n\t\t\texpectError:  true,\n\t\t\texpectOutput: \"❌ Pipeline file is invalid\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid pipeline with wrong type\",\n\t\t\tfileContent: `steps:\n  - label: 123\n    command: echo \"Hello, world!\"`,\n\t\t\texpectError:  true,\n\t\t\texpectOutput: \"❌ Pipeline file is invalid\",\n\t\t},\n\t\t{\n\t\t\tname:         \"empty file\",\n\t\t\tfileContent:  \"\",\n\t\t\texpectError:  true,\n\t\t\texpectOutput: \"File is empty\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid YAML syntax\",\n\t\t\tfileContent: `steps:\n  - label: \"This has invalid syntax\n    command: echo \"Missing closing quote and improper indentation`,\n\t\t\texpectError:  true,\n\t\t\texpectOutput: \"YAML parsing error\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttest := test\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpFile, err := os.CreateTemp(\"\", \"pipeline-*.yaml\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tdefer os.Remove(tmpFile.Name())\n\n\t\t\tif _, err := tmpFile.Write([]byte(test.fileContent)); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif err := tmpFile.Close(); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tvar stdout bytes.Buffer\n\n\t\t\tmockValidatePipeline := func(w io.Writer, filePath string) error {\n\t\t\t\tpipelineData, err := os.ReadFile(filePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error reading pipeline file: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif len(strings.TrimSpace(string(pipelineData))) == 0 {\n\t\t\t\t\tfmt.Fprintf(w, \"❌ Pipeline file is invalid: %s\\n\\n\", filePath)\n\t\t\t\t\tfmt.Fprintf(w, \"- File is empty\\n\")\n\t\t\t\t\treturn fmt.Errorf(\"empty pipeline file\")\n\t\t\t\t}\n\n\t\t\t\tjsonData, err := yaml.YAMLToJSON(pipelineData)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Fprintf(w, \"❌ Pipeline file is invalid: %s\\n\\n\", filePath)\n\t\t\t\t\tfmt.Fprintf(w, \"- YAML parsing error: %s\\n\", err.Error())\n\t\t\t\t\tfmt.Fprintf(w, \"  Hint: Check for syntax errors like improper indentation, missing quotes, or invalid characters.\\n\")\n\t\t\t\t\treturn fmt.Errorf(\"invalid YAML format: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tdocumentLoader := gojsonschema.NewBytesLoader(jsonData)\n\t\t\t\tresult, err := gojsonschema.Validate(testSchemaLoader, documentLoader)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error validating pipeline: %w\", err)\n\t\t\t\t}\n\n\t\t\t\tif result.Valid() {\n\t\t\t\t\tfmt.Fprintf(w, \"✅ Pipeline file is valid: %s\\n\", filePath)\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tfmt.Fprintf(w, \"❌ Pipeline file is invalid: %s\\n\\n\", filePath)\n\t\t\t\tfor _, err := range result.Errors() {\n\t\t\t\t\tmessage := formatValidationError(err)\n\t\t\t\t\tfmt.Fprintf(w, \"- %s\\n\", message)\n\t\t\t\t}\n\n\t\t\t\treturn fmt.Errorf(\"pipeline validation failed\")\n\t\t\t}\n\n\t\t\terr = mockValidatePipeline(&stdout, tmpFile.Name())\n\n\t\t\tif test.expectError {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"Expected error but got none\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"Expected no error but got: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !strings.Contains(stdout.String(), test.expectOutput) {\n\t\t\t\tt.Errorf(\"Expected output to contain %q, but got: %q\", test.expectOutput, stdout.String())\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:tparallel // they change dir, so let them run one at a time to avoid flakiness\nfunc TestFindPipelineFile(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no file exists\", func(t *testing.T) {\n\t\ttmpDir := t.TempDir()\n\t\torigDir, _ := os.Getwd()\n\t\tdefer os.Chdir(origDir)\n\t\tos.Chdir(tmpDir)\n\n\t\t_, err := findPipelineFile()\n\t\tif err == nil {\n\t\t\tt.Error(\"Expected error but got none\")\n\t\t}\n\t})\n\n\tt.Run(\"find pipeline.yml\", func(t *testing.T) {\n\t\ttmpDir := t.TempDir()\n\t\tbuildkiteDir := filepath.Join(tmpDir, \".buildkite\")\n\t\tos.MkdirAll(buildkiteDir, 0o755)\n\n\t\ttestFile := filepath.Join(buildkiteDir, \"pipeline.yml\")\n\t\tos.WriteFile(testFile, []byte(\"steps: []\"), 0o644)\n\n\t\torigDir, _ := os.Getwd()\n\t\tdefer os.Chdir(origDir)\n\t\tos.Chdir(tmpDir)\n\n\t\tpath, err := findPipelineFile()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Expected no error but got: %v\", err)\n\t\t}\n\n\t\texpected := filepath.Join(\".buildkite\", \"pipeline.yml\")\n\t\tif path != expected {\n\t\t\tt.Errorf(\"Expected %q but got %q\", expected, path)\n\t\t}\n\t})\n\n\tt.Run(\"find pipeline.yaml\", func(t *testing.T) {\n\t\ttmpDir := t.TempDir()\n\t\tbuildkiteDir := filepath.Join(tmpDir, \".buildkite\")\n\t\tos.MkdirAll(buildkiteDir, 0o755)\n\n\t\ttestFile := filepath.Join(buildkiteDir, \"pipeline.yaml\")\n\t\tos.WriteFile(testFile, []byte(\"steps: []\"), 0o644)\n\n\t\torigDir, _ := os.Getwd()\n\t\tdefer os.Chdir(origDir)\n\t\tos.Chdir(tmpDir)\n\n\t\tpath, err := findPipelineFile()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Expected no error but got: %v\", err)\n\t\t}\n\n\t\tif !strings.Contains(path, \"pipeline.y\") {\n\t\t\tt.Errorf(\"Expected path to contain pipeline.yaml or pipeline.yml, got %q\", path)\n\t\t}\n\t})\n}\n\n// Test formatting validation errors\nfunc TestFormatValidationError(t *testing.T) {\n\tt.Parallel()\n\n\t// This is a simplified test since we can't directly create gojsonschema.ResultError objects\n\t// Create a test schema that we can use to generate validation errors\n\tschemaJSON := []byte(`{\n\t\t\"type\": \"object\",\n\t\t\"properties\": {\n\t\t\t\"steps\": {\n\t\t\t\t\"type\": \"array\",\n\t\t\t\t\"items\": {\n\t\t\t\t\t\"type\": \"object\",\n\t\t\t\t\t\"properties\": {\n\t\t\t\t\t\t\"label\": { \"type\": \"string\" },\n\t\t\t\t\t\t\"command\": { \"type\": \"string\" }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}`)\n\n\t// Invalid YAML that will cause validation errors\n\tinvalidYaml := `\nsteps:\n  - label: \"Test\"\n    command: echo \"Hello\"\n  - label: \"Test 2\"\n    command: 123\n`\n\n\t// Load the pipeline file\n\ttmpFile, err := os.CreateTemp(\"\", \"invalid-pipeline-*.yaml\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer os.Remove(tmpFile.Name())\n\n\tif _, err := tmpFile.Write([]byte(invalidYaml)); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := tmpFile.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar buf bytes.Buffer\n\n\tpipelineData, err := os.ReadFile(tmpFile.Name())\n\tif err != nil {\n\t\tt.Fatalf(\"error reading pipeline file: %v\", err)\n\t}\n\n\t// Convert YAML to JSON for validation\n\tjsonData, err := yaml.YAMLToJSON(pipelineData)\n\tif err != nil {\n\t\tt.Fatalf(\"error converting YAML to JSON: %v\", err)\n\t}\n\n\t// Validate using our test schema\n\tschemaLoader := gojsonschema.NewBytesLoader(schemaJSON)\n\tdocumentLoader := gojsonschema.NewBytesLoader(jsonData)\n\tresult, err := gojsonschema.Validate(schemaLoader, documentLoader)\n\tif err != nil {\n\t\tt.Fatalf(\"error validating pipeline: %v\", err)\n\t}\n\n\t// Output the validation errors to the buffer\n\tfmt.Fprintf(&buf, \"❌ Pipeline file is invalid: %s\\n\\n\", tmpFile.Name())\n\tfor _, err := range result.Errors() {\n\t\tmessage := formatValidationError(err)\n\t\tfmt.Fprintf(&buf, \"- %s\\n\", message)\n\t}\n\n\toutput := buf.String()\n\n\t// Output should mention the error for the invalid field type\n\tif !strings.Contains(output, \"invalid\") {\n\t\tt.Errorf(\"Expected output to mention invalid field, got: %s\", output)\n\t}\n}\n\nfunc TestFileExists(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"file exists\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttmpFile, err := os.CreateTemp(\"\", \"test-*.txt\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tdefer os.Remove(tmpFile.Name())\n\t\ttmpFile.Close()\n\n\t\tif !fileExists(tmpFile.Name()) {\n\t\t\tt.Error(\"Expected file to exist\")\n\t\t}\n\t})\n\n\tt.Run(\"file does not exist\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tif fileExists(\"/this/path/does/not/exist/file.txt\") {\n\t\t\tt.Error(\"Expected file to not exist\")\n\t\t}\n\t})\n\n\tt.Run(\"directory exists but not a file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttmpDir := t.TempDir()\n\n\t\tif fileExists(tmpDir) {\n\t\t\tt.Error(\"Expected fileExists to return false for directory\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "cmd/pipeline/view.go",
    "content": "package pipeline\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\t\"github.com/pkg/browser\"\n)\n\ntype ViewCmd struct {\n\t// Pipeline is the positional arg; PipelineFlag (--pipeline/-p) takes priority when both are provided.\n\tPipeline     string `arg:\"\" help:\"The pipeline to view. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.\" optional:\"\"`\n\tPipelineFlag string `help:\"The pipeline to view. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.\" short:\"p\" name:\"pipeline\"`\n\tOrg          string `help:\"Organization slug.\" name:\"org\"`\n\tWeb          bool   `help:\"Open the pipeline in a web browser.\" short:\"w\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ViewCmd) Help() string {\n\treturn `View information about a pipeline.\n\nExamples:\n  # View a pipeline\n  $ bk pipeline view my-pipeline\n\n  # View a pipeline using flags\n  $ bk pipeline view --org my-org --pipeline my-pipeline\n\n  # View a pipeline in a specific organization\n  $ bk pipeline view my-org/my-pipeline\n\n  # Open pipeline in browser\n  $ bk pipeline view my-pipeline --web\n\n  # Output as JSON\n  $ bk pipeline view my-pipeline -o json\n`\n}\n\nfunc (c *ViewCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()), factory.WithOrgOverride(c.Org))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfigurationForOrg(f.Config, kongCtx.Command(), c.Org); err != nil {\n\t\treturn err\n\t}\n\n\tif c.Pipeline != \"\" && c.PipelineFlag != \"\" && c.Pipeline != c.PipelineFlag {\n\t\treturn fmt.Errorf(\"pipeline provided as both positional argument (%q) and --pipeline flag (%q); use only one\", c.Pipeline, c.PipelineFlag)\n\t}\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tpipelineArg := c.PipelineFlag\n\tif pipelineArg == \"\" {\n\t\tpipelineArg = c.Pipeline\n\t}\n\n\tvar args []string\n\tif pipelineArg != \"\" {\n\t\targs = []string{pipelineArg}\n\t}\n\n\tpicker := resolver.PickOneWithFactory(f)\n\tcachedPicker := resolver.CachedPicker(f.Config, picker)\n\trepositoryResolver := resolver.ResolveFromRepository(f, cachedPicker)\n\tif c.Org != \"\" {\n\t\trepositoryResolver = resolver.ResolveFromRepositoryInOrg(f, cachedPicker, c.Org)\n\t}\n\n\tpipelineRes := resolver.NewAggregateResolver(\n\t\tresolver.WithOrg(c.Org, resolver.ResolveFromPositionalArgument(args, 0, f.Config)),\n\t\tresolver.WithOrg(c.Org, resolver.ResolveFromConfig(f.Config, picker)),\n\t\trepositoryResolver,\n\t)\n\n\tpipeline, err := pipelineRes.Resolve(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tslug := fmt.Sprintf(\"%s/%s\", pipeline.Org, pipeline.Name)\n\n\tif c.Web {\n\t\treturn browser.OpenURL(fmt.Sprintf(\"https://buildkite.com/%s\", slug))\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tvar p buildkite.Pipeline\n\tif err = bkIO.SpinWhile(f, \"Loading pipeline information\", func() error {\n\t\tvar apiErr error\n\t\tp, _, apiErr = f.RestAPIClient.Pipelines.Get(ctx, pipeline.Org, pipeline.Name)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\tpipelineView := output.Viewable[buildkite.Pipeline]{\n\t\tData:   p,\n\t\tRender: renderPipelineText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, pipelineView, format)\n\t}\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\treturn output.Write(writer, pipelineView, format)\n}\n\nfunc renderPipelineText(p buildkite.Pipeline) string {\n\trows := [][]string{\n\t\t{\"Description\", output.ValueOrDash(p.Description)},\n\t\t{\"Repository\", output.ValueOrDash(p.Repository)},\n\t\t{\"Default Branch\", output.ValueOrDash(p.DefaultBranch)},\n\t\t{\"Visibility\", output.ValueOrDash(p.Visibility)},\n\t\t{\"Web URL\", output.ValueOrDash(p.WebURL)},\n\t}\n\n\tif len(p.Tags) > 0 {\n\t\trows = append(rows, []string{\"Tags\", strings.Join(p.Tags, \", \")})\n\t}\n\n\tif p.ClusterID != \"\" {\n\t\trows = append(rows, []string{\"Cluster ID\", p.ClusterID})\n\t}\n\n\tvar sb strings.Builder\n\tfmt.Fprintf(&sb, \"Viewing %s\\n\\n\", output.ValueOrDash(p.Name))\n\n\ttable := output.Table(\n\t\t[]string{\"Field\", \"Value\"},\n\t\trows,\n\t\tmap[string]string{\"field\": \"dim\", \"value\": \"italic\"},\n\t)\n\n\tsb.WriteString(table)\n\n\tif p.Configuration != \"\" {\n\t\tsb.WriteString(\"\\n\\nConfiguration:\\n\")\n\t\tsb.WriteString(p.Configuration)\n\t}\n\n\treturn sb.String()\n}\n"
  },
  {
    "path": "cmd/pkg/push.go",
    "content": "package pkg\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/internal/util\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/go-buildkite/v4\"\n)\n\nvar (\n\tErrInvalidConfig = errors.New(\"invalid config\")\n\tErrAPIError      = errors.New(\"API error\")\n\n\t// To be overridden in testing\n\t// Actually diddling an io.Reader so that it looks like a readable stdin is tricky\n\t// so we'll just stub this out\n\tisStdInReadableFunc = isStdinReadable\n)\n\ntype PushCmd struct {\n\tRegistrySlug  string `arg:\"\" required:\"\" help:\"The slug of the registry to push the package to\" `\n\tFilePath      string `xor:\"input\" help:\"Path to the package file to push\"`\n\tStdinFileName string `xor:\"input\" help:\"The filename to use when reading the package from stdin\"`\n\tStdInArg      string `arg:\"\" optional:\"\" hidden:\"\" help:\"Use '-' as value to pass package via stdin. Required if --stdin-file-name is used.\"`\n\tWeb           bool   `short:\"w\" help:\"Open the pipeline in a web browser.\" `\n}\n\nfunc (c *PushCmd) Help() string {\n\treturn `Push a new package to a Buildkite registry. The package can be passed as a path to a file with the --file-path flag,\nor via stdin. If passed via stdin, the filename must be provided with the --stdin-file-name flag, as a Buildkite\nregistry requires a filename for the package.\n\nExamples:\n\tPush a package to a Buildkite registry\n\tThe web URL of the uploaded package will be printed to stdout.\n\n\t# Push package from file\n\t$ bk package push my-registry --file-path my-package.tar.gz\n\n\t# Push package via stdin\n\t$ cat my-package.tar.gz | bk package push my-registry --stdin-file-name my-package.tar.gz - # Pass package via stdin, note hyphen as the argument\n\n\t# add -w to open the build in your web browser\n\t$ bk package push my-registry --file-path my-package.tar.gz -w\n`\n}\n\nfunc (c *PushCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\terr = c.Validate()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to validate flags and args: %w\", err)\n\t}\n\n\tvar (\n\t\tfrom        io.Reader\n\t\tpackageName string\n\t)\n\n\tswitch {\n\tcase c.FilePath != \"\":\n\t\tpackageName = c.FilePath\n\t\tfile, err := os.Open(c.FilePath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"couldn't open file %s: %w\", c.FilePath, err)\n\t\t}\n\t\tdefer file.Close()\n\n\t\tfrom = file\n\tcase c.StdinFileName != \"\":\n\t\tpackageName = c.StdinFileName\n\t\tfrom = os.Stdin\n\n\tdefault:\n\t\tpanic(\"Neither file path nor stdin file name are available, there has been an error in the config validation. Report this to support@buildkite.com\")\n\t}\n\n\tctx := context.Background()\n\tvar pkg buildkite.Package\n\tif err = bkIO.SpinWhile(f, \"Pushing file\", func() error {\n\t\tvar apiErr error\n\t\tpkg, _, apiErr = f.RestAPIClient.PackagesService.Create(ctx, f.Config.OrganizationSlug(), c.RegistrySlug, buildkite.CreatePackageInput{\n\t\t\tFilename: packageName,\n\t\t\tPackage:  from,\n\t\t})\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"%w: request to create package failed: %w\", ErrAPIError, err)\n\t}\n\n\treturn util.OpenInWebBrowser(c.Web, pkg.WebURL)\n}\n\nfunc isStdinReadable() (bool, error) {\n\tstat, err := os.Stdin.Stat()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to stat stdin: %w\", err)\n\t}\n\n\treadable := (stat.Mode() & os.ModeCharDevice) == 0\n\treturn readable, nil\n}\n\nfunc (c *PushCmd) Validate() error {\n\t// Validate the args such that either a file path is provided or stdin is being used\n\n\t// check if c.FilePath and c.Stdin cannot be both set or both empty\n\tif c.FilePath == \"\" && c.StdinFileName == \"\" {\n\t\treturn fmt.Errorf(\"%w: either a file path argument or --stdin-file-name must be provided\", ErrInvalidConfig)\n\t}\n\n\tif c.FilePath != \"\" && c.StdinFileName != \"\" {\n\t\treturn fmt.Errorf(\"%w: cannot provide both a file path argument and --stdin-file-name\", ErrInvalidConfig)\n\t}\n\n\tif c.StdinFileName != \"\" {\n\t\tif c.StdInArg != \"-\" {\n\t\t\treturn fmt.Errorf(\"%w:  when passing a package file via stdin, the final argument must be '-'\", ErrInvalidConfig)\n\t\t}\n\n\t\tstdInReadable, err := isStdInReadableFunc()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to check if stdin is readable: %w\", err)\n\t\t}\n\n\t\tif !stdInReadable {\n\t\t\treturn fmt.Errorf(\"%w: stdin is not readable\", ErrInvalidConfig)\n\t\t}\n\n\t\treturn nil\n\t} else {\n\t\t// Validate if an std-in arg is provided without stdin-file-name\n\t\tif c.StdInArg == \"-\" {\n\t\t\treturn fmt.Errorf(\"%w: when passing a package file via stdin, --stdin-file-name must be provided\", ErrInvalidConfig)\n\t\t}\n\t\t// We have a file path, check it exists and is a regular file\n\t\tfi, err := os.Stat(c.FilePath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"%w: %w\", ErrInvalidConfig, err)\n\t\t}\n\n\t\tif !fi.Mode().IsRegular() {\n\t\t\tmode := \"directory\"\n\t\t\tif !fi.Mode().IsDir() {\n\t\t\t\tmode = fi.Mode().String()\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"%w: file at %s is not a regular file, mode was: %s\", ErrInvalidConfig, c.FilePath, mode)\n\t\t}\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "cmd/pkg/push_test.go",
    "content": "package pkg\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestPackagePushCommandArgs(t *testing.T) {\n\tt.Parallel()\n\n\tcases := []struct {\n\t\tname  string\n\t\tstdin io.Reader\n\t\tcmd   PushCmd\n\n\t\twantErrContain string\n\t\twantErr        error\n\t}{\n\t\t// Config validation errors\n\t\t{\n\t\t\tname: \"no args\",\n\t\t\tcmd: PushCmd{\n\t\t\t\tRegistrySlug:  \"my-registry\",\n\t\t\t\tFilePath:      \"\",\n\t\t\t\tStdinFileName: \"\",\n\t\t\t\tStdInArg:      \"\",\n\t\t\t},\n\t\t\twantErrContain: \"either a file path argument or --stdin-file-name must be provided\",\n\t\t\twantErr:        ErrInvalidConfig,\n\t\t},\n\t\t{\n\t\t\tname: \"file that's a directory\",\n\t\t\tcmd: PushCmd{\n\t\t\t\tRegistrySlug:  \"my-registry\",\n\t\t\t\tFilePath:      \"/\",\n\t\t\t\tStdinFileName: \"\",\n\t\t\t\tStdInArg:      \"\",\n\t\t\t},\n\t\t\twantErr:        ErrInvalidConfig,\n\t\t\twantErrContain: \"file at / is not a regular file, mode was: directory\",\n\t\t},\n\t\t{\n\t\t\tname: \"file that doesn't exist\",\n\t\t\tcmd: PushCmd{\n\t\t\t\tRegistrySlug:  \"my-registry\",\n\t\t\t\tFilePath:      \"/does-not-exist\",\n\t\t\t\tStdinFileName: \"\",\n\t\t\t\tStdInArg:      \"\",\n\t\t\t},\n\t\t\twantErr:        ErrInvalidConfig,\n\t\t\twantErrContain: \"stat /does-not-exist: no such file or directory\",\n\t\t},\n\t\t{\n\t\t\tname: \"cannot provide both file path and stdin file name\",\n\t\t\tcmd: PushCmd{\n\t\t\t\tRegistrySlug:  \"my-registry\",\n\t\t\t\tFilePath:      \"/a-test-package.pkg\",\n\t\t\t\tStdinFileName: \"a-test-package.pkg\",\n\t\t\t\tStdInArg:      \"\",\n\t\t\t},\n\t\t\twantErr:        ErrInvalidConfig,\n\t\t\twantErrContain: \"cannot provide both a file path argument and --stdin-file-name\",\n\t\t},\n\t\t{\n\t\t\tname: \"file path but with stdin arg '-'\",\n\t\t\tcmd: PushCmd{\n\t\t\t\tRegistrySlug:  \"my-registry\",\n\t\t\t\tFilePath:      \"/directory/test.pkg\",\n\t\t\t\tStdinFileName: \"\",\n\t\t\t\tStdInArg:      \"-\",\n\t\t\t},\n\t\t\tstdin:          strings.NewReader(\"test package stream contents!\"),\n\t\t\twantErr:        ErrInvalidConfig,\n\t\t\twantErrContain: \"when passing a package file via stdin, --stdin-file-name must be provided\",\n\t\t},\n\t\t{\n\t\t\tname: \"stdin without --stdin-file-name\",\n\t\t\tcmd: PushCmd{\n\t\t\t\tRegistrySlug:  \"my-registry\",\n\t\t\t\tFilePath:      \"\",\n\t\t\t\tStdinFileName: \"test\",\n\t\t\t\tStdInArg:      \"\",\n\t\t\t},\n\t\t\tstdin:          strings.NewReader(\"test package stream contents!\"),\n\t\t\twantErr:        ErrInvalidConfig,\n\t\t\twantErrContain: \"when passing a package file via stdin, the final argument must be '-'\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\terr := tc.cmd.Validate()\n\t\t\tif !errors.Is(err, tc.wantErr) {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\tif err != nil && !strings.Contains(err.Error(), tc.wantErrContain) {\n\t\t\t\tt.Errorf(\"Expected error to contain %q, got %q\", tc.wantErrContain, err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/preflight/cleanup_cmd.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkErrors \"github.com/buildkite/cli/v3/internal/errors\"\n\t\"github.com/buildkite/cli/v3/internal/preflight\"\n)\n\n// CleanupCmd deletes remote bk/preflight/* branches whose builds have completed.\ntype CleanupCmd struct {\n\tPipeline      string `help:\"The pipeline to check builds against. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.\" short:\"p\"`\n\tPreflightUUID string `help:\"Target a single preflight branch by its UUID (bk/preflight/<uuid>).\" name:\"preflight-uuid\"`\n\tDryRun        bool   `help:\"Show which branches would be deleted without actually deleting them.\" name:\"dry-run\"`\n\tText          bool   `help:\"Use plain text output instead of interactive terminal UI.\" xor:\"output\"`\n\tJSON          bool   `help:\"Emit one JSON object per event (JSONL).\" xor:\"output\"`\n}\n\nfunc (c *CleanupCmd) Help() string {\n\treturn `Deletes remote bk/preflight/* branches whose builds have completed (passed, failed, canceled). Branches with in-progress builds are left untouched to avoid interrupting concurrent preflight runs. Pass --preflight-uuid to target a single preflight branch.`\n}\n\nfunc (c *CleanupCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tif c.PreflightUUID != \"\" {\n\t\tif _, err := uuid.Parse(c.PreflightUUID); err != nil {\n\t\t\treturn bkErrors.NewValidationError(err, fmt.Sprintf(\"invalid preflight UUID %q\", c.PreflightUUID))\n\t\t}\n\t}\n\n\tpCtx, err := setup(c.Pipeline, globals)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer pCtx.Stop()\n\n\tctx := pCtx.Ctx\n\trepoRoot := pCtx.RepoRoot\n\tresolvedPipeline := pCtx.Pipeline\n\n\tvar branches []preflight.BranchBuild\n\tif c.PreflightUUID != \"\" {\n\t\tbranch, err := preflight.LookupRemotePreflightBranch(repoRoot, c.PreflightUUID, globals.EnableDebug())\n\t\tif err != nil {\n\t\t\treturn bkErrors.NewInternalError(err, \"failed to look up preflight branch\")\n\t\t}\n\t\tif branch != nil {\n\t\t\tbranches = []preflight.BranchBuild{*branch}\n\t\t}\n\t} else {\n\t\tbranches, err = preflight.ListRemotePreflightBranches(repoRoot, globals.EnableDebug())\n\t\tif err != nil {\n\t\t\treturn bkErrors.NewInternalError(err, \"failed to list remote preflight branches\")\n\t\t}\n\t}\n\n\tif len(branches) == 0 {\n\t\tif c.PreflightUUID != \"\" {\n\t\t\tfmt.Fprintf(os.Stdout, \"No preflight branch found for UUID %s\\n\", c.PreflightUUID)\n\t\t} else {\n\t\t\tfmt.Fprintln(os.Stdout, \"No preflight branches found\")\n\t\t}\n\t\treturn nil\n\t}\n\n\trenderer := rendererFactory(os.Stdout, c.JSON, c.Text, pCtx.Stop)\n\tdefer renderer.Close()\n\n\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: fmt.Sprintf(\"Found %d preflight branch(es), checking build status...\", len(branches))})\n\n\tif err := preflight.ResolveBuilds(ctx, pCtx.Factory.RestAPIClient, resolvedPipeline.Org, resolvedPipeline.Name, branches); err != nil {\n\t\tif errors.Is(err, context.Canceled) {\n\t\t\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: \"Cleanup interrupted\"})\n\t\t\treturn nil\n\t\t}\n\t\treturn bkErrors.NewInternalError(err, \"failed to check build status for preflight branches\")\n\t}\n\n\tvar toDelete []string\n\tvar deleted, skipped int\n\tfor i := range branches {\n\t\tbb := branches[i]\n\t\tif !bb.IsCompleted() {\n\t\t\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: fmt.Sprintf(\"Skipping %s (build state: %s)\", bb.Branch, bb.Build.State)})\n\t\t\tskipped++\n\t\t\tcontinue\n\t\t}\n\n\t\tstate := \"no build found\"\n\t\tif bb.Build != nil {\n\t\t\tstate = bb.Build.State\n\t\t}\n\n\t\tif c.DryRun {\n\t\t\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: fmt.Sprintf(\"Would delete %s (%s)\", bb.Branch, state)})\n\t\t} else {\n\t\t\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: fmt.Sprintf(\"Deleting %s (%s)\", bb.Branch, state)})\n\t\t\ttoDelete = append(toDelete, bb.Ref)\n\t\t}\n\t\tdeleted++\n\t}\n\n\tif !c.DryRun && len(toDelete) > 0 {\n\t\tif err := preflight.CleanupRefs(repoRoot, toDelete, globals.EnableDebug()); err != nil {\n\t\t\treturn bkErrors.NewInternalError(err, \"failed to delete preflight branches from remote\")\n\t\t}\n\t}\n\n\tverb := \"deleted\"\n\tif c.DryRun {\n\t\tverb = \"would delete\"\n\t}\n\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), Title: fmt.Sprintf(\"Cleanup complete: %d %s, %d skipped\", deleted, verb, skipped)})\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/preflight/cleanup_cmd_test.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\tbkErrors \"github.com/buildkite/cli/v3/internal/errors\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestCleanupCmd_Run(t *testing.T) {\n\tt.Run(\"returns validation error when experiment disabled\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"alpha\")\n\n\t\tcmd := &CleanupCmd{}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t}\n\n\t\tif !bkErrors.IsValidationError(err) {\n\t\t\tt.Fatalf(\"expected validation error, got %T: %v\", err, err)\n\t\t}\n\t})\n\n\tt.Run(\"deletes completed branches and skips running ones\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\t// Create a test repo with two preflight branches.\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\t// Create two preflight branches by pushing commits.\n\t\tcreatePreflightBranch(t, worktree, \"bk/preflight/completed-one\")\n\t\tcreatePreflightBranch(t, worktree, \"bk/preflight/still-running\")\n\n\t\t// Verify both branches exist on remote.\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif !strings.Contains(refs, \"bk/preflight/completed-one\") {\n\t\t\tt.Fatal(\"expected completed-one branch to exist\")\n\t\t}\n\t\tif !strings.Contains(refs, \"bk/preflight/still-running\") {\n\t\t\tt.Fatal(\"expected still-running branch to exist\")\n\t\t}\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tif r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tbranches := r.URL.Query()[\"branch[]\"]\n\t\t\t\tvar builds []buildkite.Build\n\t\t\t\tfor _, branch := range branches {\n\t\t\t\t\tswitch branch {\n\t\t\t\t\tcase \"bk/preflight/completed-one\":\n\t\t\t\t\t\tbuilds = append(builds, buildkite.Build{Number: 1, State: \"passed\", Branch: branch})\n\t\t\t\t\tcase \"bk/preflight/still-running\":\n\t\t\t\t\t\tbuilds = append(builds, buildkite.Build{Number: 2, State: \"running\", Branch: branch})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(builds)\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tcmd := &CleanupCmd{Pipeline: \"test-org/test-pipeline\", Text: true}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\n\t\t// Verify completed branch was deleted.\n\t\trefs = runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif strings.Contains(refs, \"bk/preflight/completed-one\") {\n\t\t\tt.Error(\"expected completed-one branch to be deleted\")\n\t\t}\n\n\t\t// Verify running branch was preserved.\n\t\tif !strings.Contains(refs, \"bk/preflight/still-running\") {\n\t\t\tt.Error(\"expected still-running branch to be preserved\")\n\t\t}\n\t})\n\n\tt.Run(\"reports no branches when none exist\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tcmd := &CleanupCmd{Pipeline: \"test-org/test-pipeline\", Text: true}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"deletes orphaned branches with no builds\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\tcreatePreflightBranch(t, worktree, \"bk/preflight/orphaned\")\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tif r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.Build{})\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tcmd := &CleanupCmd{Pipeline: \"test-org/test-pipeline\", Text: true}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif strings.Contains(refs, \"bk/preflight/orphaned\") {\n\t\t\tt.Error(\"expected orphaned branch to be deleted\")\n\t\t}\n\t})\n\n\tt.Run(\"falls back to git cli when factory has no repository\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\toriginalNewFactory := newFactory\n\t\tt.Cleanup(func() { newFactory = originalNewFactory })\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tif r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tbranches := r.URL.Query()[\"branch[]\"]\n\t\t\t\tvar builds []buildkite.Build\n\t\t\t\tfor _, branch := range branches {\n\t\t\t\t\tbuilds = append(builds, buildkite.Build{Number: 1, State: \"failed\", Branch: branch})\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(builds)\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tnewFactory = func(...factory.FactoryOpt) (*factory.Factory, error) {\n\t\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn &factory.Factory{\n\t\t\t\tConfig:        config.New(nil, nil),\n\t\t\t\tRestAPIClient: client,\n\t\t\t}, nil\n\t\t}\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\tcreatePreflightBranch(t, worktree, \"bk/preflight/to-clean\")\n\n\t\tcmd := &CleanupCmd{Pipeline: \"test-org/test-pipeline\", Text: true}\n\t\tif err := cmd.Run(nil, stubGlobals{}); err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif strings.Contains(refs, \"bk/preflight/to-clean\") {\n\t\t\tt.Error(\"expected branch to be deleted\")\n\t\t}\n\t})\n\n\tt.Run(\"returns error when API fails\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\tcreatePreflightBranch(t, worktree, \"bk/preflight/some-branch\")\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tif r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\thttp.Error(w, `{\"message\":\"internal error\"}`, http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tcmd := &CleanupCmd{Pipeline: \"test-org/test-pipeline\", Text: true}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error when API fails, got nil\")\n\t\t}\n\n\t\t// Branch should still exist since the error prevented cleanup.\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif !strings.Contains(refs, \"bk/preflight/some-branch\") {\n\t\t\tt.Error(\"expected branch to be preserved when API fails\")\n\t\t}\n\t})\n\n\tt.Run(\"dry run shows branches without deleting them\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\tcreatePreflightBranch(t, worktree, \"bk/preflight/dry-run-test\")\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tif r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tbranches := r.URL.Query()[\"branch[]\"]\n\t\t\t\tvar builds []buildkite.Build\n\t\t\t\tfor _, branch := range branches {\n\t\t\t\t\tbuilds = append(builds, buildkite.Build{Number: 1, State: \"passed\", Branch: branch})\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(builds)\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tcmd := &CleanupCmd{Pipeline: \"test-org/test-pipeline\", Text: true, DryRun: true}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\n\t\t// Branch should still exist after dry run.\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif !strings.Contains(refs, \"bk/preflight/dry-run-test\") {\n\t\t\tt.Error(\"expected branch to be preserved during dry run\")\n\t\t}\n\t})\n\n\tt.Run(\"stops processing when context is cancelled\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\tcreatePreflightBranch(t, worktree, \"bk/preflight/cancel-a\")\n\t\tcreatePreflightBranch(t, worktree, \"bk/preflight/cancel-b\")\n\n\t\t// Override notifyContext to return an already-cancelled context so\n\t\t// the API call in ResolveBuilds returns context.Canceled immediately.\n\t\toriginalNotify := notifyContext\n\t\tnotifyContext = func(parent context.Context, _ ...os.Signal) (context.Context, context.CancelFunc) {\n\t\t\tctx, cancel := context.WithCancel(parent)\n\t\t\tcancel()\n\t\t\treturn ctx, cancel\n\t\t}\n\t\tt.Cleanup(func() { notifyContext = originalNotify })\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tcmd := &CleanupCmd{Pipeline: \"test-org/test-pipeline\", Text: true}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error on cancellation, got: %v\", err)\n\t\t}\n\n\t\t// Both branches should still exist since cleanup was interrupted.\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif !strings.Contains(refs, \"bk/preflight/cancel-a\") {\n\t\t\tt.Error(\"expected cancel-a to be preserved after cancellation\")\n\t\t}\n\t\tif !strings.Contains(refs, \"bk/preflight/cancel-b\") {\n\t\t\tt.Error(\"expected cancel-b to be preserved after cancellation\")\n\t\t}\n\t})\n\n\tt.Run(\"preflight-uuid targets a single branch and leaves others alone\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\tconst targetUUID = \"01935b62-0000-7000-8000-000000000001\"\n\t\tconst otherUUID = \"01935b62-0000-7000-8000-000000000002\"\n\t\ttargetBranch := \"bk/preflight/\" + targetUUID\n\t\totherBranch := \"bk/preflight/\" + otherUUID\n\t\tcreatePreflightBranch(t, worktree, targetBranch)\n\t\tcreatePreflightBranch(t, worktree, otherBranch)\n\n\t\tvar queriedBranches []string\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tif r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tqueriedBranches = append(queriedBranches, r.URL.Query()[\"branch[]\"]...)\n\t\t\t\tvar builds []buildkite.Build\n\t\t\t\tfor _, branch := range r.URL.Query()[\"branch[]\"] {\n\t\t\t\t\tbuilds = append(builds, buildkite.Build{Number: 1, State: \"passed\", Branch: branch})\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(builds)\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tcmd := &CleanupCmd{Pipeline: \"test-org/test-pipeline\", Text: true, PreflightUUID: targetUUID}\n\t\tif err := cmd.Run(nil, stubGlobals{}); err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\n\t\t// Only the targeted branch should have been queried for build state.\n\t\tif len(queriedBranches) != 1 || queriedBranches[0] != targetBranch {\n\t\t\tt.Errorf(\"expected build lookup scoped to %s, got %v\", targetBranch, queriedBranches)\n\t\t}\n\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif strings.Contains(refs, targetBranch) {\n\t\t\tt.Errorf(\"expected targeted branch %s to be deleted\", targetBranch)\n\t\t}\n\t\tif !strings.Contains(refs, otherBranch) {\n\t\t\tt.Errorf(\"expected untargeted branch %s to be preserved\", otherBranch)\n\t\t}\n\t})\n\n\tt.Run(\"preflight-uuid with invalid UUID returns validation error\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tcmd := &CleanupCmd{Pipeline: \"test-org/test-pipeline\", Text: true, PreflightUUID: \"not-a-uuid\"}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected validation error, got nil\")\n\t\t}\n\t\tif !bkErrors.IsValidationError(err) {\n\t\t\tt.Fatalf(\"expected validation error, got %T: %v\", err, err)\n\t\t}\n\t})\n\n\tt.Run(\"preflight-uuid with no matching branch exits cleanly\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\t// An unrelated preflight branch exists, but the targeted one does not.\n\t\tcreatePreflightBranch(t, worktree, \"bk/preflight/01935b62-0000-7000-8000-000000000003\")\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tt.Errorf(\"unexpected API request: %s %s\", r.Method, r.URL.Path)\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tcmd := &CleanupCmd{\n\t\t\tPipeline:      \"test-org/test-pipeline\",\n\t\t\tText:          true,\n\t\t\tPreflightUUID: \"01935b62-0000-7000-8000-0000000000ff\",\n\t\t}\n\t\tif err := cmd.Run(nil, stubGlobals{}); err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\n\t\t// The unrelated branch should still exist.\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif !strings.Contains(refs, \"bk/preflight/01935b62-0000-7000-8000-000000000003\") {\n\t\t\tt.Error(\"expected unrelated preflight branch to be preserved\")\n\t\t}\n\t})\n}\n\n// createPreflightBranch creates a preflight branch on the remote by pushing a commit.\nfunc createPreflightBranch(t *testing.T, worktree, branch string) {\n\tt.Helper()\n\n\t// Create a file and commit it on a temporary local branch.\n\tfile := filepath.Join(worktree, \"preflight-marker.txt\")\n\tif err := os.WriteFile(file, []byte(branch+\"\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\trunGit(t, worktree, \"add\", \"preflight-marker.txt\")\n\trunGit(t, worktree, \"commit\", \"-m\", \"preflight snapshot for \"+branch)\n\n\t// Push HEAD to the preflight branch on origin, then reset back.\n\tcommit := runGit(t, worktree, \"rev-parse\", \"HEAD\")\n\trunGit(t, worktree, \"push\", \"origin\", commit+\":refs/heads/\"+branch)\n\trunGit(t, worktree, \"reset\", \"--hard\", \"HEAD~1\")\n}\n"
  },
  {
    "path": "cmd/preflight/event.go",
    "content": "package preflight\n\nimport (\n\t\"time\"\n\n\t\"github.com/buildkite/cli/v3/internal/build/watch\"\n\tinternalpreflight \"github.com/buildkite/cli/v3/internal/preflight\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// EventType identifies the kind of preflight event.\ntype EventType string\n\nconst (\n\tEventOperation      EventType = \"operation\"\n\tEventBuildStatus    EventType = \"build_status\"\n\tEventJobFailure     EventType = \"job_failure\"\n\tEventJobRetryPassed EventType = \"job_retry_passed\"\n\tEventBuildSummary   EventType = \"build_summary\"\n\tEventTestFailure    EventType = \"test_failure\"\n)\n\n// Event is the single data model emitted by a preflight run.\n// Renderers project events differently by output mode (TTY, text, JSON).\ntype Event struct {\n\tType EventType `json:\"type\"`\n\tTime time.Time `json:\"timestamp\"`\n\n\tPreflightID string `json:\"preflight_id,omitempty\"`\n\n\t// Title is the primary status text shown in the TTY dynamic area.\n\tTitle string `json:\"title,omitempty\"`\n\n\t// Detail is supplementary information printed to the scrollback log.\n\tDetail string `json:\"detail,omitempty\"`\n\n\tPipeline    string `json:\"pipeline,omitempty\"`\n\tBuildNumber int    `json:\"build_number,omitempty\"`\n\tBuildURL    string `json:\"build_url,omitempty\"`\n\tBuildState  string `json:\"build_state,omitempty\"`\n\n\t// Incomplete is set for build_summary events when the CLI stops before a terminal build state.\n\tIncomplete bool `json:\"incomplete,omitempty\"`\n\n\t// StopReason describes why the summary was emitted early.\n\tStopReason string `json:\"stop_reason,omitempty\"`\n\n\t// BuildCanceled is set when the CLI attempted early-exit cleanup that cancels the remote build.\n\tBuildCanceled *bool `json:\"build_canceled,omitempty\"`\n\n\tJobs *watch.JobSummary `json:\"jobs,omitempty\"`\n\n\t// Job is set for job_failure and job_retry_passed events.\n\tJob *buildkite.Job `json:\"job,omitempty\"`\n\n\t// FailedJobs is set for build_summary events when the build failed. Contains hard-failed jobs only (soft failures excluded).\n\tFailedJobs []buildkite.Job `json:\"failed_jobs,omitempty\"`\n\n\t// PassedJobs is set for build_summary events when the build passed and has 10 or fewer jobs.\n\tPassedJobs []buildkite.Job `json:\"passed_jobs,omitempty\"`\n\n\t// Duration is set for build_summary events. Total elapsed time of the preflight run.\n\tDuration time.Duration `json:\"duration_ns,omitempty\"`\n\n\t// TestFailures is set for test_failure events.\n\tTestFailures []buildkite.BuildTest `json:\"test_failures,omitempty\"`\n\n\t// Tests is set for build_summary events when aggregated test summary data is available.\n\tTests internalpreflight.SummaryTests `json:\"tests,omitempty\"`\n}\n\nfunc newBuildSummaryEvent(preflightID, pipeline string, buildNumber int, buildURL string, finalBuild buildkite.Build, startedAt time.Time) Event {\n\treturn Event{\n\t\tType:        EventBuildSummary,\n\t\tTime:        time.Now(),\n\t\tPreflightID: preflightID,\n\t\tPipeline:    pipeline,\n\t\tBuildNumber: buildNumber,\n\t\tBuildURL:    buildURL,\n\t\tBuildState:  finalBuild.State,\n\t\tDuration:    time.Since(startedAt),\n\t}\n}\n\nfunc (e *Event) ApplySummaryMeta(meta summaryMeta) {\n\te.Incomplete = meta.Incomplete\n\te.StopReason = meta.StopReason\n\n\tif meta.StopReason == \"\" {\n\t\treturn\n\t}\n\n\tbuildCanceled := meta.BuildCanceled\n\te.BuildCanceled = &buildCanceled\n}\n\nfunc (e *Event) ApplyJobResults(finalBuild buildkite.Build, tracker *watch.JobTracker) {\n\tif NewResult(finalBuild).Passed() {\n\t\tif passed := tracker.PassedJobs(); len(passed) <= 10 {\n\t\t\te.PassedJobs = passed\n\t\t}\n\t\treturn\n\t}\n\n\te.FailedJobs = tracker.FailedJobs()\n}\n"
  },
  {
    "path": "cmd/preflight/event_test.go",
    "content": "package preflight\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/buildkite/cli/v3/internal/build/watch\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestEvent_Operation(t *testing.T) {\n\te := Event{\n\t\tType:        EventOperation,\n\t\tTime:        time.Now(),\n\t\tPreflightID: \"preflight-123\",\n\t\tTitle:       \"Creating snapshot of working tree...\",\n\t}\n\n\tif e.Type != EventOperation {\n\t\tt.Fatalf(\"expected EventOperation, got %q\", e.Type)\n\t}\n\tif e.Title == \"\" {\n\t\tt.Fatal(\"expected Title to be set\")\n\t}\n\tif e.BuildState != \"\" {\n\t\tt.Fatal(\"expected BuildState to be empty for operation event\")\n\t}\n}\n\nfunc TestEvent_BuildStatus(t *testing.T) {\n\te := Event{\n\t\tType:        EventBuildStatus,\n\t\tTime:        time.Now(),\n\t\tPreflightID: \"preflight-123\",\n\t\tPipeline:    \"buildkite/cli\",\n\t\tBuildNumber: 42,\n\t\tBuildURL:    \"https://buildkite.com/buildkite/cli/builds/42\",\n\t\tBuildState:  \"running\",\n\t\tJobs: &watch.JobSummary{\n\t\t\tPassed:  8,\n\t\t\tRunning: 3,\n\t\t},\n\t}\n\n\tif e.Type != EventBuildStatus {\n\t\tt.Fatalf(\"expected EventBuildStatus, got %q\", e.Type)\n\t}\n\tif e.BuildNumber != 42 {\n\t\tt.Fatalf(\"expected BuildNumber 42, got %d\", e.BuildNumber)\n\t}\n\tif e.Jobs.Passed != 8 {\n\t\tt.Fatalf(\"expected 8 passed, got %d\", e.Jobs.Passed)\n\t}\n}\n\nfunc TestEvent_JobFailure(t *testing.T) {\n\te := Event{\n\t\tType:        EventJobFailure,\n\t\tTime:        time.Now(),\n\t\tPreflightID: \"preflight-123\",\n\t\tPipeline:    \"buildkite/cli\",\n\t\tBuildNumber: 42,\n\t\tBuildState:  \"failing\",\n\t\tJob: &buildkite.Job{\n\t\t\tID:    \"job-1\",\n\t\t\tName:  \"Lint\",\n\t\t\tState: \"failed\",\n\t\t},\n\t}\n\n\tif e.Type != EventJobFailure {\n\t\tt.Fatalf(\"expected EventJobFailure, got %q\", e.Type)\n\t}\n\tif e.Job == nil {\n\t\tt.Fatal(\"expected Job to be set\")\n\t}\n\tif e.Job.ID != \"job-1\" {\n\t\tt.Fatalf(\"expected job ID job-1, got %q\", e.Job.ID)\n\t}\n}\n\nfunc TestEvent_BuildSummaryStoppedEarly(t *testing.T) {\n\tbuildCanceled := false\n\te := Event{\n\t\tType:          EventBuildSummary,\n\t\tTime:          time.Now(),\n\t\tPreflightID:   \"preflight-123\",\n\t\tPipeline:      \"buildkite/cli\",\n\t\tBuildNumber:   42,\n\t\tBuildState:    \"failing\",\n\t\tIncomplete:    true,\n\t\tStopReason:    \"build-failing\",\n\t\tBuildCanceled: &buildCanceled,\n\t}\n\n\tif e.Type != EventBuildSummary {\n\t\tt.Fatalf(\"expected EventBuildSummary, got %q\", e.Type)\n\t}\n\tif !e.Incomplete {\n\t\tt.Fatal(\"expected Incomplete to be set\")\n\t}\n\tif e.StopReason != \"build-failing\" {\n\t\tt.Fatalf(\"expected stop reason build-failing, got %q\", e.StopReason)\n\t}\n\tif e.BuildCanceled == nil || *e.BuildCanceled {\n\t\tt.Fatalf(\"expected BuildCanceled=false, got %#v\", e.BuildCanceled)\n\t}\n}\n"
  },
  {
    "path": "cmd/preflight/job_presenter.go",
    "content": "package preflight\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/buildkite/cli/v3/internal/build/watch\"\n\t\"github.com/buildkite/cli/v3/internal/emoji\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\ntype jobPresenter struct {\n\tpipeline    string\n\tbuildNumber int\n\tbuildURL    string\n}\n\nfunc (p jobPresenter) failParts(j buildkite.Job, action string) (symbol, name, detail string) {\n\tjob := watch.NewFormattedJob(j)\n\tname = job.DisplayName()\n\n\tvar status string\n\tswitch {\n\tcase job.IsSoftFailed():\n\t\tstatus = \"soft failed\"\n\tdefault:\n\t\tstatus = j.State\n\t}\n\n\tif j.ExitStatus != nil && *j.ExitStatus != 0 {\n\t\tstatus += fmt.Sprintf(\" with exit %d\", *j.ExitStatus)\n\t}\n\n\tsymbol = \"✗\"\n\tif job.IsSoftFailed() {\n\t\tsymbol = \"⚠\"\n\t}\n\n\tdetail = fmt.Sprintf(\"%s - %s\", status, action)\n\treturn symbol, name, detail\n}\n\nfunc (p jobPresenter) Line(j buildkite.Job) string {\n\tsymbol, name, detail := p.failParts(j, jobLogCommand(p.pipeline, p.buildNumber, j.ID))\n\treturn fmt.Sprintf(\"%s %s %s\", symbol, name, detail)\n}\n\nfunc (p jobPresenter) PassedLine(j buildkite.Job) string {\n\tname := watch.NewFormattedJob(j).DisplayName()\n\treturn fmt.Sprintf(\"✔ %s\", name)\n}\n\nfunc (p jobPresenter) RetryPassedLine(j buildkite.Job) string {\n\tname := watch.NewFormattedJob(j).DisplayName()\n\treturn fmt.Sprintf(\"✔ %s passed on retry (attempt %d)\", name, j.RetriesCount+1)\n}\n\nfunc (p jobPresenter) ColoredRetryPassedLine(j buildkite.Job) string {\n\temojiPrefix, textName := emoji.Split(watch.NewFormattedJob(j).DisplayName())\n\tstyle := lipgloss.NewStyle().Foreground(lipgloss.Color(\"2\"))\n\tdetail := fmt.Sprintf(\"passed on retry (attempt %d)\", j.RetriesCount+1)\n\tif emojiPrefix != \"\" {\n\t\treturn style.Render(\"✔ \") + emoji.Render(emojiPrefix) + \" \" + style.Render(fmt.Sprintf(\"%s %s\", textName, detail))\n\t}\n\treturn style.Render(fmt.Sprintf(\"✔ %s %s\", textName, detail))\n}\n\nfunc (p jobPresenter) ColoredPassedLine(j buildkite.Job, style lipgloss.Style) string {\n\temojiPrefix, textName := emoji.Split(watch.NewFormattedJob(j).DisplayName())\n\tif emojiPrefix != \"\" {\n\t\treturn style.Render(\"✔ \") + emoji.Render(emojiPrefix) + \" \" + style.Render(textName)\n\t}\n\treturn style.Render(fmt.Sprintf(\"✔ %s\", textName))\n}\n\n// ColoredLine renders emoji outside the ANSI colour span to avoid\n// Kitty/iTerm2 graphics escape sequences breaking lipgloss styling.\nfunc (p jobPresenter) ColoredLine(j buildkite.Job) string {\n\tjob := watch.NewFormattedJob(j)\n\tsymbol, name, detail := p.failParts(j, p.jobLink(j))\n\n\temojiPrefix, textName := emoji.Split(name)\n\n\tstyle := lipgloss.NewStyle().Foreground(lipgloss.Color(\"9\"))\n\tif job.IsSoftFailed() {\n\t\tstyle = lipgloss.NewStyle().Foreground(lipgloss.Color(\"11\"))\n\t}\n\n\tif emojiPrefix != \"\" {\n\t\treturn style.Render(symbol+\" \") + emoji.Render(emojiPrefix) + \" \" + style.Render(fmt.Sprintf(\"%s %s\", textName, detail))\n\t}\n\treturn style.Render(fmt.Sprintf(\"%s %s %s\", symbol, textName, detail))\n}\n\nfunc (p jobPresenter) jobLink(j buildkite.Job) string {\n\turl := j.WebURL\n\tif url == \"\" && p.buildURL != \"\" && j.ID != \"\" {\n\t\turl = p.buildURL + \"#\" + j.ID\n\t}\n\tif url == \"\" {\n\t\treturn jobLogCommand(p.pipeline, p.buildNumber, j.ID)\n\t}\n\treturn terminalHyperlink(\"\\033[4:4mView job\\033[24m\", url)\n}\n"
  },
  {
    "path": "cmd/preflight/job_presenter_test.go",
    "content": "package preflight\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestJobPresenter_FailedLine(t *testing.T) {\n\tstartedAt := buildkite.Timestamp{Time: time.Now().Add(-90 * time.Second)}\n\tfinishedAt := buildkite.Timestamp{Time: time.Now().Add(-15 * time.Second)}\n\texitStatus := 1\n\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 183663,\n\t}.Line(scriptJob(\"failed-windows-smoke-tests\", \"Windows smoke tests\", \"failed\", false, &startedAt, &finishedAt, &exitStatus))\n\n\tassertStringContainsAll(t, line, []string{\n\t\t\"✗ Windows smoke tests\",\n\t\t\"failed with exit 1\",\n\t\t\"- bk job log -b 183663 -p buildkite/cli failed-windows-smoke-tests\",\n\t})\n}\n\nfunc TestJobPresenter_SoftFailedLine(t *testing.T) {\n\tstartedAt := buildkite.Timestamp{Time: time.Now().Add(-90 * time.Second)}\n\tfinishedAt := buildkite.Timestamp{Time: time.Now().Add(-15 * time.Second)}\n\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite\",\n\t\tbuildNumber: 183663,\n\t}.Line(scriptJob(\"failed-2\", \"Bundle Audit\", \"failed\", true, &startedAt, &finishedAt, nil))\n\n\tassertStringContainsAll(t, line, []string{\n\t\t\"⚠ Bundle Audit\",\n\t\t\"soft failed\",\n\t\t\"- bk job log -b 183663 -p buildkite failed-2\",\n\t})\n}\n\nfunc TestJobPresenter_FailedNoExit(t *testing.T) {\n\tstartedAt := buildkite.Timestamp{Time: time.Now().Add(-90 * time.Second)}\n\tfinishedAt := buildkite.Timestamp{Time: time.Now().Add(-15 * time.Second)}\n\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t}.Line(scriptJob(\"job-1\", \"Lint\", \"failed\", false, &startedAt, &finishedAt, nil))\n\n\tassertStringContainsAll(t, line, []string{\n\t\t\"✗ Lint\",\n\t\t\"failed\",\n\t\t\"- bk job log -b 42 -p buildkite/cli job-1\",\n\t})\n\tif strings.Contains(line, \"with exit\") {\n\t\tt.Fatalf(\"did not expect exit status when nil: %q\", line)\n\t}\n}\n\nfunc TestJobPresenter_PassedLine(t *testing.T) {\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t}.PassedLine(buildkite.Job{ID: \"j1\", Name: \"Lint\", Type: \"script\", State: \"passed\"})\n\n\tassertStringContainsAll(t, line, []string{\"✔ Lint\"})\n}\n\nfunc TestJobPresenter_PassedLine_WithEmoji(t *testing.T) {\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t}.PassedLine(buildkite.Job{ID: \"j1\", Name: \":checkered_flag: Feature flags\", Type: \"script\", State: \"passed\"})\n\n\tif !strings.Contains(line, \"✔\") {\n\t\tt.Fatalf(\"expected check mark in %q\", line)\n\t}\n\tif !strings.Contains(line, \"Feature flags\") {\n\t\tt.Fatalf(\"expected job name in %q\", line)\n\t}\n}\n\nfunc TestJobPresenter_RetryPassedLine(t *testing.T) {\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t}.RetryPassedLine(buildkite.Job{ID: \"retry-1\", Name: \"Lint\", Type: \"script\", State: \"passed\", RetriesCount: 1})\n\n\tassertStringContainsAll(t, line, []string{\"✔ Lint\", \"passed on retry\", \"attempt 2\"})\n}\n\nfunc TestJobPresenter_RetryPassedLine_MultipleRetries(t *testing.T) {\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t}.RetryPassedLine(buildkite.Job{ID: \"retry-2\", Name: \"Test\", Type: \"script\", State: \"passed\", RetriesCount: 2})\n\n\tassertStringContainsAll(t, line, []string{\"✔ Test\", \"passed on retry\", \"attempt 3\"})\n}\n\nfunc TestJobPresenter_ColoredRetryPassedLine(t *testing.T) {\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t}.ColoredRetryPassedLine(buildkite.Job{ID: \"retry-1\", Name: \"Lint\", Type: \"script\", State: \"passed\", RetriesCount: 1})\n\n\tassertStringContainsAll(t, line, []string{\"✔\", \"Lint\", \"passed on retry\", \"attempt 2\"})\n}\n\nfunc TestJobPresenter_ColoredRetryPassedLine_WithEmoji(t *testing.T) {\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t}.ColoredRetryPassedLine(buildkite.Job{ID: \"retry-1\", Name: \":docker: Build image\", Type: \"script\", State: \"passed\", RetriesCount: 1})\n\n\tassertStringContainsAll(t, line, []string{\"✔\", \"Build image\", \"passed on retry\"})\n}\n\nfunc TestJobPresenter_ColoredLine(t *testing.T) {\n\tstartedAt := buildkite.Timestamp{Time: time.Now().Add(-90 * time.Second)}\n\tfinishedAt := buildkite.Timestamp{Time: time.Now().Add(-15 * time.Second)}\n\texitStatus := 1\n\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t}.ColoredLine(scriptJob(\"job-1\", \"Test\", \"failed\", false, &startedAt, &finishedAt, &exitStatus))\n\n\tassertStringContainsAll(t, line, []string{\"✗\", \"Test\", \"failed with exit 1\"})\n}\n\nfunc TestJobPresenter_ColoredLine_UsesClickableJobLink(t *testing.T) {\n\tstartedAt := buildkite.Timestamp{Time: time.Now().Add(-90 * time.Second)}\n\tfinishedAt := buildkite.Timestamp{Time: time.Now().Add(-15 * time.Second)}\n\texitStatus := 1\n\n\tjob := scriptJob(\"job-1\", \"Test\", \"failed\", false, &startedAt, &finishedAt, &exitStatus)\n\tjob.WebURL = \"https://buildkite.com/buildkite/cli/builds/42#job-1\"\n\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t}.ColoredLine(job)\n\n\tassertStringContainsAll(t, line, []string{\"✗\", \"Test\", \"failed with exit 1 - \", \"\\033[4:4mView job\\033[24m\", job.WebURL})\n\tif strings.Contains(line, \"bk job log\") {\n\t\tt.Fatalf(\"expected clickable job link instead of job log command: %q\", line)\n\t}\n}\n\nfunc TestJobPresenter_ColoredLine_DerivesClickableJobLinkFromBuildURL(t *testing.T) {\n\tstartedAt := buildkite.Timestamp{Time: time.Now().Add(-90 * time.Second)}\n\tfinishedAt := buildkite.Timestamp{Time: time.Now().Add(-15 * time.Second)}\n\texitStatus := 1\n\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t\tbuildURL:    \"https://buildkite.com/buildkite/cli/builds/42\",\n\t}.ColoredLine(scriptJob(\"job-1\", \"Test\", \"failed\", false, &startedAt, &finishedAt, &exitStatus))\n\n\tassertStringContainsAll(t, line, []string{\"View job\", \"https://buildkite.com/buildkite/cli/builds/42#job-1\"})\n\tif strings.Contains(line, \"bk job log\") {\n\t\tt.Fatalf(\"expected derived clickable job link instead of job log command: %q\", line)\n\t}\n}\n\nfunc TestJobPresenter_ColoredLine_SoftFailed(t *testing.T) {\n\tstartedAt := buildkite.Timestamp{Time: time.Now().Add(-90 * time.Second)}\n\tfinishedAt := buildkite.Timestamp{Time: time.Now().Add(-15 * time.Second)}\n\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t}.ColoredLine(scriptJob(\"job-1\", \"Audit\", \"failed\", true, &startedAt, &finishedAt, nil))\n\n\tassertStringContainsAll(t, line, []string{\"⚠\", \"Audit\", \"soft failed\"})\n}\n\nfunc TestJobPresenter_ColoredLine_WithEmoji(t *testing.T) {\n\tstartedAt := buildkite.Timestamp{Time: time.Now().Add(-90 * time.Second)}\n\tfinishedAt := buildkite.Timestamp{Time: time.Now().Add(-15 * time.Second)}\n\texitStatus := 1\n\n\tline := jobPresenter{\n\t\tpipeline:    \"buildkite/cli\",\n\t\tbuildNumber: 42,\n\t}.ColoredLine(scriptJob(\"job-1\", \":docker: Build image\", \"failed\", false, &startedAt, &finishedAt, &exitStatus))\n\n\tassertStringContainsAll(t, line, []string{\"✗\", \"Build image\", \"failed with exit 1\"})\n}\n\nfunc assertStringContainsAll(t *testing.T, got string, want []string) {\n\tt.Helper()\n\n\tfor _, needle := range want {\n\t\tif !strings.Contains(got, needle) {\n\t\t\tt.Fatalf(\"missing %q in %q\", needle, got)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cmd/preflight/preflight.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/buildkite/cli/v3/cmd/version\"\n\tbuildstate \"github.com/buildkite/cli/v3/internal/build/state\"\n\t\"github.com/buildkite/cli/v3/internal/build/watch\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tinternalconfig \"github.com/buildkite/cli/v3/internal/config\"\n\tbkErrors \"github.com/buildkite/cli/v3/internal/errors\"\n\tbkhttp \"github.com/buildkite/cli/v3/internal/http\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\tinternalpreflight \"github.com/buildkite/cli/v3/internal/preflight\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype RunCmd struct {\n\tPipeline         string                         `help:\"The pipeline to build. This can be a {pipeline slug} or in the format {org slug}/{pipeline slug}.\" short:\"p\"`\n\tWatch            bool                           `help:\"Watch the build until completion.\" default:\"true\" negatable:\"\"`\n\tExitOn           []internalpreflight.ExitPolicy `help:\"Exit when a condition is met. Options: build-failing (default, exits when a build enters the failing state), build-terminal (exits when the build reaches a terminal state).\"`\n\tInterval         float64                        `help:\"Polling interval in seconds when watching.\" default:\"2\"`\n\tNoCleanup        bool                           `help:\"Skip cleanup after completion or early exit. The preflight branch remains and the build keeps running if exiting early.\"`\n\tAwaitTestResults awaitTestResultsFlag           `name:\"await-test-results\" help:\"After the build finishes, wait for test results to be processed by Test Engine. Provide a duration like 10s, or omit the value to wait 30s.\"`\n\tText             bool                           `help:\"Use plain text output instead of interactive terminal UI.\" xor:\"output\"`\n\tJSON             bool                           `help:\"Emit one JSON object per event (JSONL).\" xor:\"output\"`\n}\n\nvar (\n\tnotifyContext   = signal.NotifyContext\n\tnewFactory      = factory.New\n\trendererFactory = newRenderer\n\n\terrExitOnBuildFailing = errors.New(\"exit-on build-failing\")\n)\n\nconst defaultAwaitTestResultsDuration = 30 * time.Second\n\nfunc HelpText() string {\n\treturn `Preflight is an experimental preview and subject to change without notice.\n\nSnapshots your working tree (staged, unstaged, and untracked files) to a temporary commit on a bk/preflight/<id> branch, triggers a build on the selected pipeline, monitors failures, exits as soon as the build starts failing, and cleans up the temporary branch when finished.`\n}\n\ntype summaryMeta struct {\n\tIncomplete    bool\n\tStopReason    string\n\tBuildCanceled bool\n}\n\nfunc preflightUserAgentSuffix() string {\n\tmajor := strings.TrimPrefix(version.Version, \"v\")\n\tif i := strings.IndexByte(major, '.'); i >= 0 {\n\t\tmajor = major[:i]\n\t}\n\tif major == \"\" || major == \"DEV\" {\n\t\tmajor = \"DEV\"\n\t}\n\treturn \"buildkite-cli-preflight/\" + major + \".x\"\n}\n\ntype awaitTestResultsFlag struct {\n\tEnabled  bool\n\tDuration time.Duration\n}\n\nfunc (f *awaitTestResultsFlag) Decode(ctx *kong.DecodeContext) error {\n\tvar value string\n\tif err := ctx.Scan.PopValueInto(\"duration\", &value); err != nil {\n\t\tf.Enabled = true\n\t\tf.Duration = defaultAwaitTestResultsDuration\n\t\treturn nil\n\t}\n\n\tduration, err := time.ParseDuration(value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.Enabled = true\n\tf.Duration = duration\n\treturn nil\n}\n\nfunc (f awaitTestResultsFlag) IsBool() bool { return true }\n\nfunc (c *RunCmd) Help() string {\n\treturn HelpText()\n}\n\nfunc (c *RunCmd) Validate() error {\n\tif c.Interval <= 0 {\n\t\treturn bkErrors.NewValidationError(fmt.Errorf(\"interval must be greater than 0\"), \"invalid polling interval\")\n\t}\n\treturn internalpreflight.ValidateExitPolicies(c.ExitOn, c.Watch)\n}\n\nfunc (c *RunCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tif err := c.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\texitPolicy := internalpreflight.EffectiveExitPolicy(c.ExitOn)\n\n\tpCtx, err := setup(c.Pipeline, globals)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer pCtx.Stop()\n\n\tf := pCtx.Factory\n\trepoRoot := pCtx.RepoRoot\n\tresolvedPipeline := pCtx.Pipeline\n\tctx := pCtx.Ctx\n\tstop := pCtx.Stop\n\trlTransport := pCtx.RateLimitTransport\n\n\tpreflightID, err := uuid.NewV7()\n\tif err != nil {\n\t\treturn bkErrors.NewInternalError(err, \"UUIDv7 generation failed\")\n\t}\n\tstartedAt := time.Now()\n\n\tsourceContext, err := internalpreflight.ResolveSourceContext(repoRoot, globals.EnableDebug())\n\tif err != nil {\n\t\treturn bkErrors.NewValidationError(\n\t\t\terr,\n\t\t\t\"failed to resolve preflight source git context\",\n\t\t\t\"Ensure the repository has at least one commit\",\n\t\t)\n\t}\n\n\trenderer := rendererFactory(os.Stdout, c.JSON, c.Text, stop)\n\tdefer renderer.Close()\n\n\trlTransport.OnRateLimit = func(attempt int, delay time.Duration) {\n\t\tif globals.EnableDebug() {\n\t\t\t_ = renderer.Render(Event{\n\t\t\t\tType:        EventOperation,\n\t\t\t\tTime:        time.Now(),\n\t\t\t\tPreflightID: preflightID.String(),\n\t\t\t\tTitle:       fmt.Sprintf(\"Rate limited by API, waiting %s before retrying (attempt %d/%d)...\", delay.Truncate(time.Second), attempt+1, rlTransport.MaxRetries),\n\t\t\t})\n\t\t}\n\t}\n\n\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), PreflightID: preflightID.String(), Title: \"Pushing snapshot of working tree...\"})\n\n\tvar opts []internalpreflight.SnapshotOption\n\tif globals.EnableDebug() {\n\t\topts = append(opts, internalpreflight.WithDebug())\n\t}\n\n\tresult, err := internalpreflight.SnapshotContext(ctx, repoRoot, preflightID, opts...)\n\tif err != nil {\n\t\tif errors.Is(err, context.Canceled) || errors.Is(ctx.Err(), context.Canceled) {\n\t\t\treturn bkErrors.NewUserAbortedError(context.Canceled, \"preflight canceled by user\")\n\t\t}\n\t\treturn bkErrors.NewSnapshotError(\n\t\t\terr, \"failed to create preflight snapshot\",\n\t\t\t\"Ensure you have uncommitted or committed changes to snapshot\",\n\t\t\t\"Ensure you have push access to the remote repository\",\n\t\t)\n\t}\n\n\tsnapshotDetail := fmt.Sprintf(\"Commit: %s\\nRef: %s\", result.ShortCommit(), result.Ref)\n\tif len(result.Files) > 0 {\n\t\tsnapshotDetail += fmt.Sprintf(\"\\nFiles:  %d changed\", len(result.Files))\n\t\tfor _, file := range result.Files {\n\t\t\tsnapshotDetail += fmt.Sprintf(\"\\n %s %s\", file.StatusSymbol(), file.Path)\n\t\t}\n\t}\n\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), PreflightID: preflightID.String(), Title: \"Pushed snapshot of working tree...\", Detail: snapshotDetail})\n\n\tcleanupBranch := func() {\n\t\tif c.NoCleanup {\n\t\t\treturn\n\t\t}\n\t\tcleanupRemoteBranch(renderer, repoRoot, result.Branch, result.Ref, preflightID.String(), globals.EnableDebug())\n\t}\n\n\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), PreflightID: preflightID.String(), Title: fmt.Sprintf(\"Creating build on %s/%s...\", resolvedPipeline.Org, resolvedPipeline.Name)})\n\n\tenv := map[string]string{\n\t\t\"PREFLIGHT\":               \"true\",\n\t\t\"BUILDKITE_PREFLIGHT\":     \"true\", // deprecated\n\t\t\"PREFLIGHT_SOURCE_COMMIT\": sourceContext.Commit,\n\t}\n\tif sourceContext.Branch != \"\" {\n\t\tenv[\"PREFLIGHT_SOURCE_BRANCH\"] = sourceContext.Branch\n\t}\n\n\tbuild, _, err := f.RestAPIClient.Builds.Create(ctx, resolvedPipeline.Org, resolvedPipeline.Name, buildkite.CreateBuild{\n\t\tMessage: fmt.Sprintf(\"Preflight %s\", preflightID),\n\t\tCommit:  result.Commit,\n\t\tBranch:  result.Branch,\n\t\tEnv:     env,\n\t})\n\tif err != nil {\n\t\tif errors.Is(err, context.Canceled) || errors.Is(ctx.Err(), context.Canceled) {\n\t\t\tcleanupBranch()\n\t\t\treturn bkErrors.NewUserAbortedError(context.Canceled, \"preflight canceled by user\")\n\t\t}\n\t\treturn bkErrors.WrapAPIError(err, \"creating preflight build\")\n\t}\n\n\tpipelineName := fmt.Sprintf(\"%s/%s\", resolvedPipeline.Org, resolvedPipeline.Name)\n\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), PreflightID: preflightID.String(), Title: fmt.Sprintf(\"Created build on %s/%s...\", resolvedPipeline.Org, resolvedPipeline.Name), Detail: fmt.Sprintf(\"Build:  %s\", build.WebURL)})\n\n\tif !c.Watch {\n\t\treturn nil\n\t}\n\n\tinterval := time.Duration(c.Interval * float64(time.Second))\n\ttracker := watch.NewJobTracker()\n\n\tfinalBuild, err := watch.WatchBuild(ctx, f.RestAPIClient, resolvedPipeline.Org, resolvedPipeline.Name, build.Number, interval, func(b buildkite.Build) error {\n\t\tstatus := tracker.Update(b)\n\t\tfor _, failed := range status.NewlyFailed {\n\t\t\tif err := renderer.Render(Event{\n\t\t\t\tType:        EventJobFailure,\n\t\t\t\tTime:        time.Now(),\n\t\t\t\tPreflightID: preflightID.String(),\n\t\t\t\tPipeline:    pipelineName,\n\t\t\t\tBuildNumber: build.Number,\n\t\t\t\tBuildURL:    build.WebURL,\n\t\t\t\tJob:         &failed,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tfor _, retryPassed := range status.NewlyRetryPassed {\n\t\t\tif err := renderer.Render(Event{\n\t\t\t\tType:        EventJobRetryPassed,\n\t\t\t\tTime:        time.Now(),\n\t\t\t\tPreflightID: preflightID.String(),\n\t\t\t\tPipeline:    pipelineName,\n\t\t\t\tBuildNumber: build.Number,\n\t\t\t\tBuildURL:    build.WebURL,\n\t\t\t\tJob:         &retryPassed,\n\t\t\t}); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif err := renderer.Render(Event{\n\t\t\tType:        EventBuildStatus,\n\t\t\tTime:        time.Now(),\n\t\t\tPreflightID: preflightID.String(),\n\t\t\tPipeline:    pipelineName,\n\t\t\tBuildNumber: build.Number,\n\t\t\tBuildURL:    build.WebURL,\n\t\t\tBuildState:  b.State,\n\t\t\tJobs:        &status.Summary,\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif exitPolicy == internalpreflight.ExitOnBuildFailing && buildstate.State(b.State) == buildstate.Failing {\n\t\t\treturn errExitOnBuildFailing\n\t\t}\n\t\treturn nil\n\t}, watch.WithRetriedJobs())\n\n\tfinalErr := NewResult(finalBuild).Error()\n\n\tif errors.Is(err, context.Canceled) {\n\t\tcleanupBranch()\n\t\tif finalBuild.FinishedAt == nil && !buildstate.IsTerminal(buildstate.State(finalBuild.State)) && !c.NoCleanup {\n\t\t\tcancelBuild(f, renderer, resolvedPipeline.Org, resolvedPipeline.Name, build.Number, preflightID.String(), globals.EnableDebug())\n\t\t}\n\t\treturn bkErrors.NewUserAbortedError(context.Canceled, \"preflight canceled by user\")\n\t}\n\n\tif errors.Is(err, errExitOnBuildFailing) {\n\t\tbuildCanceled := false\n\t\tif !c.NoCleanup {\n\t\t\tbuildCanceled = cancelBuild(f, renderer, resolvedPipeline.Org, resolvedPipeline.Name, build.Number, preflightID.String(), globals.EnableDebug())\n\t\t}\n\n\t\tsummaryEvent := newBuildSummaryEvent(preflightID.String(), pipelineName, build.Number, build.WebURL, finalBuild, startedAt)\n\t\tsummaryEvent.ApplySummaryMeta(summaryMeta{Incomplete: true, StopReason: \"build-failing\", BuildCanceled: buildCanceled})\n\t\tsummaryEvent.ApplyJobResults(finalBuild, tracker)\n\t\tshowResult, showErr := c.loadFinalResult(ctx, f.RestAPIClient, resolvedPipeline.Org, resolvedPipeline.Name, build.Number)\n\t\tif showErr == nil {\n\t\t\tsummaryEvent.Tests = showResult.Tests\n\t\t} else if globals.EnableDebug() {\n\t\t\t_ = renderer.Render(Event{\n\t\t\t\tType:        EventOperation,\n\t\t\t\tTime:        time.Now(),\n\t\t\t\tPreflightID: preflightID.String(),\n\t\t\t\tTitle:       fmt.Sprintf(\"Debug: failed to load final test summary: %v\", showErr),\n\t\t\t})\n\t\t}\n\t\t_ = renderer.Render(summaryEvent)\n\t\tcleanupBranch()\n\t\treturn finalErr\n\t}\n\n\t// Emit a final summary showing pass/fail, passed jobs (if ≤10), or hard-failed jobs.\n\tif buildstate.IsTerminal(buildstate.State(finalBuild.State)) {\n\t\tsummaryEvent := newBuildSummaryEvent(preflightID.String(), pipelineName, build.Number, build.WebURL, finalBuild, startedAt)\n\t\tsummaryEvent.ApplySummaryMeta(summaryMeta{})\n\t\tsummaryEvent.ApplyJobResults(finalBuild, tracker)\n\n\t\tshowResult, showErr := c.loadFinalResult(ctx, f.RestAPIClient, resolvedPipeline.Org, resolvedPipeline.Name, build.Number)\n\t\tif showErr == nil {\n\t\t\tsummaryEvent.Tests = showResult.Tests\n\t\t} else if globals.EnableDebug() {\n\t\t\t_ = renderer.Render(Event{\n\t\t\t\tType:        EventOperation,\n\t\t\t\tTime:        time.Now(),\n\t\t\t\tPreflightID: preflightID.String(),\n\t\t\t\tTitle:       fmt.Sprintf(\"Debug: failed to load final test summary: %v\", showErr),\n\t\t\t})\n\t\t}\n\t\t_ = renderer.Render(summaryEvent)\n\t}\n\n\tcleanupBranch()\n\n\tif err != nil {\n\t\treturn bkErrors.NewInternalError(\n\t\t\terr, \"watching build failed\",\n\t\t\t\"Buildkite API may be unavailable or your network may be unstable\",\n\t\t\t\"Retry the preflight command once connectivity is restored\",\n\t\t)\n\t}\n\n\treturn finalErr\n}\n\nfunc cleanupRemoteBranch(renderer renderer, repoRoot, branch, ref, preflightID string, debug bool) {\n\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), PreflightID: preflightID, Title: fmt.Sprintf(\"Cleaning up remote branch %s...\", branch)})\n\tif cleanupErr := internalpreflight.Cleanup(repoRoot, ref, debug); cleanupErr != nil {\n\t\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), PreflightID: preflightID, Title: fmt.Sprintf(\"Warning: failed to delete remote branch %s: %v\", ref, cleanupErr)})\n\t\treturn\n\t}\n\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), PreflightID: preflightID, Title: fmt.Sprintf(\"Deleted remote branch %s\", branch)})\n}\n\nfunc cancelBuild(f *factory.Factory, renderer renderer, org, pipeline string, buildNumber int, preflightID string, debug bool) bool {\n\tcancelCtx, cancelStop := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancelStop()\n\n\tif _, err := f.RestAPIClient.Builds.Cancel(cancelCtx, org, pipeline, strconv.Itoa(buildNumber)); err != nil {\n\t\tvar apiErr *buildkite.ErrorResponse\n\t\tif errors.As(err, &apiErr) && apiErr.Response.StatusCode == http.StatusUnprocessableEntity && apiErr.Message == \"Build can't be canceled because it's already finished.\" {\n\t\t\tif debug {\n\t\t\t\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), PreflightID: preflightID, Title: fmt.Sprintf(\"Debug: build #%d already finished, skipping cancel\", buildNumber)})\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\n\t\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), PreflightID: preflightID, Title: fmt.Sprintf(\"Warning: failed to cancel build #%d: %v\", buildNumber, err)})\n\t\treturn false\n\t}\n\n\t_ = renderer.Render(Event{Type: EventOperation, Time: time.Now(), PreflightID: preflightID, Title: fmt.Sprintf(\"Cancelled build #%d\", buildNumber)})\n\treturn true\n}\n\nfunc (c *RunCmd) loadFinalResult(ctx context.Context, client *buildkite.Client, org, pipeline string, buildNumber int) (internalpreflight.SummaryResult, error) {\n\tbuildWithTests, _, buildErr := client.Builds.Get(ctx, org, pipeline, strconv.Itoa(buildNumber), &buildkite.BuildGetOptions{IncludeTestEngine: true})\n\texpectTestSummary := buildErr == nil && buildWithTests.TestEngine != nil && len(buildWithTests.TestEngine.Runs) > 0\n\n\tif buildErr != nil {\n\t\treturn internalpreflight.SummaryResult{}, buildErr\n\t}\n\n\tif !c.AwaitTestResults.Enabled || c.AwaitTestResults.Duration <= 0 {\n\t\treturn c.loadSummary(ctx, client, org, buildWithTests.ID)\n\t}\n\tif !expectTestSummary {\n\t\treturn internalpreflight.SummaryResult{Tests: internalpreflight.SummaryTests{Runs: map[string]internalpreflight.SummaryTestRun{}, Failures: []internalpreflight.SummaryTestFailure{}}}, nil\n\t}\n\n\ttimer := time.NewTimer(c.AwaitTestResults.Duration)\n\tdefer timer.Stop()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn internalpreflight.SummaryResult{}, ctx.Err()\n\tcase <-timer.C:\n\t}\n\n\treturn c.loadSummary(ctx, client, org, buildWithTests.ID)\n}\n\nfunc (c *RunCmd) loadSummary(ctx context.Context, client *buildkite.Client, org, buildID string) (internalpreflight.SummaryResult, error) {\n\tif buildID == \"\" {\n\t\treturn internalpreflight.SummaryResult{Tests: internalpreflight.SummaryTests{Runs: map[string]internalpreflight.SummaryTestRun{}, Failures: []internalpreflight.SummaryTestFailure{}}}, nil\n\t}\n\n\tsummary, err := internalpreflight.NewRunSummaryService(client).Get(ctx, org, buildID, &internalpreflight.RunSummaryGetOptions{\n\t\tResult:          \"^failed\",\n\t\tState:           \"enabled\",\n\t\tIncludeFailures: true,\n\t})\n\tif err != nil {\n\t\treturn internalpreflight.SummaryResult{}, err\n\t}\n\n\treturn summary.SummaryResult(), nil\n}\n\n// preflightContext holds the common dependencies for preflight subcommands.\ntype preflightContext struct {\n\tFactory            *factory.Factory\n\tRepoRoot           string\n\tPipeline           *pipeline.Pipeline\n\tCtx                context.Context\n\tStop               context.CancelFunc\n\tRateLimitTransport *bkhttp.RateLimitTransport\n}\n\n// setup initializes the common preflight dependencies: factory, experiment\n// gate, repository root, signal context, and pipeline resolution.\nfunc setup(pipelineFlag string, globals cli.GlobalFlags) (*preflightContext, error) {\n\trlTransport := bkhttp.NewRateLimitTransport(http.DefaultTransport)\n\tf, err := newFactory(\n\t\tfactory.WithDebug(globals.EnableDebug()),\n\t\tfactory.WithTransport(rlTransport),\n\t\tfactory.WithUserAgentSuffix(preflightUserAgentSuffix()),\n\t)\n\tif err != nil {\n\t\treturn nil, bkErrors.NewInternalError(err, \"failed to initialize CLI\", \"This is likely a bug\", \"Report to Buildkite\")\n\t}\n\n\tif !f.Config.HasExperiment(internalconfig.ExperimentPreflight) {\n\t\treturn nil, bkErrors.NewValidationError(\n\t\t\tfmt.Errorf(\"experiment not enabled\"),\n\t\t\t\"preflight is disabled by the current experiments override. Add `preflight` to `BUILDKITE_EXPERIMENTS` or run `bk config set experiments preflight` to re-enable it\",\n\t\t)\n\t}\n\n\trepoRoot, err := resolveRepositoryRoot(f, globals.EnableDebug())\n\tif err != nil {\n\t\treturn nil, bkErrors.NewValidationError(\n\t\t\tfmt.Errorf(\"not in a git repository: %w\", err),\n\t\t\t\"preflight must be run from a git repository\",\n\t\t\t\"Run this command from inside a git repository\",\n\t\t)\n\t}\n\n\tctx, stop := notifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\n\tresolvers := resolver.NewAggregateResolver(\n\t\tresolver.ResolveFromFlag(pipelineFlag, f.Config),\n\t\tresolver.ResolveFromConfig(f.Config, resolver.PickOneWithFactory(f)),\n\t\tresolver.ResolveFromRepository(f, resolver.CachedPicker(f.Config, resolver.PickOneWithFactory(f))),\n\t)\n\n\tresolvedPipeline, err := resolvers.Resolve(ctx)\n\tif err != nil {\n\t\tstop()\n\t\treturn nil, bkErrors.NewValidationError(\n\t\t\terr, \"could not resolve a pipeline\",\n\t\t\t\"Specify a pipeline with --pipeline or link your repository to a pipeline\",\n\t\t)\n\t}\n\n\treturn &preflightContext{\n\t\tFactory:            f,\n\t\tRepoRoot:           repoRoot,\n\t\tPipeline:           resolvedPipeline,\n\t\tCtx:                ctx,\n\t\tStop:               stop,\n\t\tRateLimitTransport: rlTransport,\n\t}, nil\n}\n\nfunc resolveRepositoryRoot(f *factory.Factory, debug bool) (string, error) {\n\tif f.GitRepository != nil {\n\t\twt, err := f.GitRepository.Worktree()\n\t\tif err == nil {\n\t\t\treturn wt.Filesystem.Root(), nil\n\t\t}\n\t}\n\n\treturn internalpreflight.RepositoryRoot(\".\", debug)\n}\n"
  },
  {
    "path": "cmd/preflight/preflight_test.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\tinternalpreflight \"github.com/buildkite/cli/v3/internal/preflight\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\n\t\"github.com/buildkite/cli/v3/internal/build/watch\"\n\tbkErrors \"github.com/buildkite/cli/v3/internal/errors\"\n\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\ntype stubGlobals struct{}\n\nfunc (s stubGlobals) SkipConfirmation() bool { return false }\nfunc (s stubGlobals) DisableInput() bool     { return false }\nfunc (s stubGlobals) IsQuiet() bool          { return false }\nfunc (s stubGlobals) DisablePager() bool     { return false }\nfunc (s stubGlobals) EnableDebug() bool      { return false }\n\nvar _ cli.GlobalFlags = stubGlobals{}\n\nfunc unsetEnv(t *testing.T, key string) {\n\tt.Helper()\n\n\toriginal, had := os.LookupEnv(key)\n\tif err := os.Unsetenv(key); err != nil {\n\t\tt.Fatalf(\"failed to unset env %s: %v\", key, err)\n\t}\n\n\tt.Cleanup(func() {\n\t\tvar err error\n\t\tif had {\n\t\t\terr = os.Setenv(key, original)\n\t\t} else {\n\t\t\terr = os.Unsetenv(key)\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to restore env %s: %v\", key, err)\n\t\t}\n\t})\n}\n\nfunc TestParseExitConditions(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tcmd         RunCmd\n\t\twantPolicy  internalpreflight.ExitPolicy\n\t\twantErrText string\n\t}{\n\t\t{name: \"defaults to build-failing\", cmd: RunCmd{Watch: true, Interval: 1}, wantPolicy: internalpreflight.ExitOnBuildFailing},\n\t\t{name: \"accepts build-failing\", cmd: RunCmd{Watch: true, Interval: 1, ExitOn: []internalpreflight.ExitPolicy{internalpreflight.ExitOnBuildFailing}}, wantPolicy: internalpreflight.ExitOnBuildFailing},\n\t\t{name: \"accepts build-terminal\", cmd: RunCmd{Watch: true, Interval: 1, ExitOn: []internalpreflight.ExitPolicy{internalpreflight.ExitOnBuildTerminal}}, wantPolicy: internalpreflight.ExitOnBuildTerminal},\n\t\t{name: \"accepts repeated build-terminal\", cmd: RunCmd{Watch: true, Interval: 1, ExitOn: []internalpreflight.ExitPolicy{internalpreflight.ExitOnBuildTerminal, internalpreflight.ExitOnBuildTerminal}}, wantPolicy: internalpreflight.ExitOnBuildTerminal},\n\t\t{name: \"rejects mixed lifecycle policies\", cmd: RunCmd{Watch: true, Interval: 1, ExitOn: []internalpreflight.ExitPolicy{internalpreflight.ExitOnBuildFailing, internalpreflight.ExitOnBuildTerminal}}, wantErrText: \"build-failing and build-terminal cannot be used together\"},\n\t\t{name: \"rejects exit-on when watch disabled\", cmd: RunCmd{Watch: false, Interval: 1, ExitOn: []internalpreflight.ExitPolicy{internalpreflight.ExitOnBuildFailing}}, wantErrText: \"--exit-on requires --watch\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.cmd.Validate()\n\t\t\tif tt.wantErrText != \"\" {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t\t\t}\n\t\t\t\tif !errors.Is(err, bkErrors.ErrValidation) {\n\t\t\t\t\tt.Fatalf(\"expected validation error, got %T: %v\", err, err)\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(err.Error(), tt.wantErrText) {\n\t\t\t\t\tt.Fatalf(\"expected error containing %q, got %q\", tt.wantErrText, err.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t\t\t}\n\t\t\tgot := internalpreflight.EffectiveExitPolicy(tt.cmd.ExitOn)\n\t\t\tif got != tt.wantPolicy {\n\t\t\t\tt.Fatalf(\"policy = %v, want %v\", got, tt.wantPolicy)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPreflightCmd_Run(t *testing.T) {\n\tt.Run(\"returns validation error when experiment disabled\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"alpha\")\n\n\t\tcmd := &RunCmd{Interval: 1}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t}\n\n\t\tvar bkErr *bkErrors.Error\n\t\tif !errors.As(err, &bkErr) {\n\t\t\tt.Fatalf(\"expected bkErrors.Error, got %T: %v\", err, err)\n\t\t}\n\t\tif !errors.Is(bkErr, bkErrors.ErrValidation) {\n\t\t\tt.Errorf(\"expected ErrValidation, got category: %v\", bkErr.Category)\n\t\t}\n\t\tif !strings.Contains(bkErr.Details, \"preflight is disabled\") {\n\t\t\tt.Errorf(\"expected disabled experiment validation, got details %q\", bkErr.Details)\n\t\t}\n\t})\n\n\tt.Run(\"preflight is enabled by default\", func(t *testing.T) {\n\t\tunsetEnv(t, \"BUILDKITE_EXPERIMENTS\")\n\n\t\t// Run from a temp dir that is not a git repo. This should pass the\n\t\t// experiment gate and fail on repository validation instead.\n\t\tt.Chdir(t.TempDir())\n\n\t\tcmd := &RunCmd{Interval: 1}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t}\n\n\t\tvar bkErr *bkErrors.Error\n\t\tif !errors.As(err, &bkErr) {\n\t\t\tt.Fatalf(\"expected bkErrors.Error, got %T: %v\", err, err)\n\t\t}\n\t\tif !errors.Is(bkErr, bkErrors.ErrValidation) {\n\t\t\tt.Errorf(\"expected ErrValidation, got category: %v\", bkErr.Category)\n\t\t}\n\t\tif !strings.Contains(bkErr.Details, \"git repository\") {\n\t\t\tt.Errorf(\"expected git repository validation after default experiment gate, got details %q\", bkErr.Details)\n\t\t}\n\t})\n\n\tt.Run(\"build-failing early exit enriches summary with test results\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar buildCancelRequests atomic.Int32\n\t\tvar buildPolls atomic.Int32\n\t\tvar summaryRequests atomic.Int32\n\t\tvar includeLatestFail atomic.Bool\n\t\tvar stateEnabled atomic.Bool\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodPut && strings.Contains(r.URL.Path, \"/builds/1/cancel\"):\n\t\t\t\tbuildCancelRequests.Add(1)\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{Number: 1, State: \"canceling\"})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodPost && strings.Contains(r.URL.Path, \"/builds\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\tpoll := buildPolls.Add(1)\n\t\t\t\texitOne := 1\n\t\t\t\tbuild := buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"running\",\n\t\t\t\t\tJobs: []buildkite.Job{{\n\t\t\t\t\t\tID:    \"job-running\",\n\t\t\t\t\t\tType:  \"script\",\n\t\t\t\t\t\tName:  \"Lint\",\n\t\t\t\t\t\tState: \"running\",\n\t\t\t\t\t}},\n\t\t\t\t}\n\t\t\t\tif poll >= 2 {\n\t\t\t\t\tbuild.State = \"failing\"\n\t\t\t\t\tbuild.TestEngine = &buildkite.TestEngineProperty{\n\t\t\t\t\t\tRuns: []buildkite.TestEngineRun{{\n\t\t\t\t\t\t\tID: \"run-1\",\n\t\t\t\t\t\t\tSuite: buildkite.TestEngineSuite{\n\t\t\t\t\t\t\t\tSlug: \"rspec\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t}\n\t\t\t\t\tbuild.Jobs = []buildkite.Job{{\n\t\t\t\t\t\tID:         \"job-failed\",\n\t\t\t\t\t\tType:       \"script\",\n\t\t\t\t\t\tName:       \"Lint\",\n\t\t\t\t\t\tState:      \"failed\",\n\t\t\t\t\t\tExitStatus: &exitOne,\n\t\t\t\t\t}}\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(build)\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/tests\"):\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.BuildTest{})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/v2/analytics/organizations/test-org/builds/build-id-123/preflight/v1\":\n\t\t\t\tsummaryRequests.Add(1)\n\t\t\t\tif r.URL.Query().Get(\"include\") == \"latest_fail\" {\n\t\t\t\t\tincludeLatestFail.Store(true)\n\t\t\t\t}\n\t\t\t\tif r.URL.Query().Get(\"state\") == \"enabled\" {\n\t\t\t\t\tstateEnabled.Store(true)\n\t\t\t\t}\n\t\t\t\t_, _ = w.Write([]byte(`{\n\t\t\t\t\t\"tests\": {\n\t\t\t\t\t\t\"runs\": {\n\t\t\t\t\t\t\t\"run-1\": {\n\t\t\t\t\t\t\t\t\"suite\": {\"id\": \"suite-1\", \"slug\": \"rspec\", \"name\": \"RSpec\"},\n\t\t\t\t\t\t\t\t\"passed\": 47,\n\t\t\t\t\t\t\t\t\"failed\": 1,\n\t\t\t\t\t\t\t\t\"skipped\": 12\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"failures\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"run_id\": \"run-1\",\n\t\t\t\t\t\t\t\t\"suite_name\": \"RSpec\",\n\t\t\t\t\t\t\t\t\"suite_slug\": \"rspec\",\n\t\t\t\t\t\t\t\t\"name\": \"AuthService.validateToken handles expired tokens\",\n\t\t\t\t\t\t\t\t\"location\": \"src/auth.test.ts:89\",\n\t\t\t\t\t\t\t\t\"latest_fail\": {\n\t\t\t\t\t\t\t\t\t\"failure_reason\": \"Expected 'expired' but got 'invalid'\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}`))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tstdout := captureStdout(t, func() {\n\t\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01, JSON: true}\n\t\t\terr := cmd.Run(nil, stubGlobals{})\n\t\t\tvar bkErr *bkErrors.Error\n\t\t\tif !errors.As(err, &bkErr) || !errors.Is(bkErr, bkErrors.ErrPreflightIncompleteFailure) {\n\t\t\t\tt.Fatalf(\"expected incomplete failure error, got %v\", err)\n\t\t\t}\n\t\t})\n\n\t\tevents := decodeJSONLEvents(t, stdout)\n\t\tvar buildStatusCount int\n\t\tvar summaries []Event\n\t\tfor _, event := range events {\n\t\t\tif event.Type == EventBuildStatus {\n\t\t\t\tbuildStatusCount++\n\t\t\t}\n\t\t\tif event.Type == EventBuildSummary {\n\t\t\t\tsummaries = append(summaries, event)\n\t\t\t}\n\t\t}\n\n\t\tif buildStatusCount != 2 {\n\t\t\tt.Fatalf(\"expected 2 build status events before early stop, got %d\", buildStatusCount)\n\t\t}\n\t\tif len(summaries) != 1 {\n\t\t\tt.Fatalf(\"expected exactly 1 build summary event, got %d\", len(summaries))\n\t\t}\n\t\tsummary := summaries[0]\n\t\tif !summary.Incomplete {\n\t\t\tt.Fatal(\"expected summary to be marked incomplete\")\n\t\t}\n\t\tif summary.StopReason != \"build-failing\" {\n\t\t\tt.Fatalf(\"expected stop reason build-failing, got %q\", summary.StopReason)\n\t\t}\n\t\tif summary.BuildCanceled == nil || !*summary.BuildCanceled {\n\t\t\tt.Fatalf(\"expected build_canceled=true, got %#v\", summary.BuildCanceled)\n\t\t}\n\t\tif summary.BuildState != \"failing\" {\n\t\t\tt.Fatalf(\"expected failing build state, got %q\", summary.BuildState)\n\t\t}\n\t\tif len(summary.FailedJobs) != 1 || summary.FailedJobs[0].Name != \"Lint\" {\n\t\t\tt.Fatalf(\"expected failed jobs in summary, got %#v\", summary.FailedJobs)\n\t\t}\n\t\tif got := summary.Tests.Runs[\"run-1\"]; got.SuiteName != \"RSpec\" || got.Failed != 1 || got.Passed != 47 || got.Skipped != 12 {\n\t\t\tt.Fatalf(\"expected enriched test run summary, got %#v\", got)\n\t\t}\n\t\tif len(summary.Tests.Failures) != 1 || summary.Tests.Failures[0].Name != \"AuthService.validateToken handles expired tokens\" {\n\t\t\tt.Fatalf(\"expected enriched test failures, got %#v\", summary.Tests.Failures)\n\t\t}\n\t\tif !includeLatestFail.Load() {\n\t\t\tt.Fatal(\"expected early-exit summary to request latest_fail details\")\n\t\t}\n\t\tif !stateEnabled.Load() {\n\t\t\tt.Fatal(\"expected early-exit summary to request state=enabled\")\n\t\t}\n\t\tif summaryRequests.Load() != 1 {\n\t\t\tt.Fatalf(\"expected one preflight summary request, got %d\", summaryRequests.Load())\n\t\t}\n\t\tif buildCancelRequests.Load() != 1 {\n\t\t\tt.Fatalf(\"expected one build cancel request, got %d\", buildCancelRequests.Load())\n\t\t}\n\t\tif buildPolls.Load() != 3 {\n\t\t\tt.Fatalf(\"expected three build polls including final summary fetch, got %d\", buildPolls.Load())\n\t\t}\n\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif strings.Contains(refs, \"bk/preflight/\") {\n\t\t\tt.Errorf(\"expected preflight branch to be cleaned up, but found: %s\", refs)\n\t\t}\n\t})\n\n\tt.Run(\"snapshots and creates build\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar gotReq buildkite.CreateBuild\n\t\tvar gotUserAgent string\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method == \"POST\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tgotUserAgent = r.Header.Get(\"User-Agent\")\n\t\t\t\tjson.NewDecoder(r.Body).Decode(&gotReq)\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:      \"build-id-123\",\n\t\t\t\t\tNumber:  1,\n\t\t\t\t\tState:   \"scheduled\",\n\t\t\t\t\tWebURL:  \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t\tMessage: gotReq.Message,\n\t\t\t\t\tCommit:  gotReq.Commit,\n\t\t\t\t\tBranch:  gotReq.Branch,\n\t\t\t\t\tURL:     \"https://api.buildkite.com/v2/organizations/test-org/pipelines/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\t// Create a dirty file so the snapshot has something to commit.\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\texpectedSourceBranch := runGit(t, worktree, \"branch\", \"--show-current\")\n\t\texpectedSourceCommit := runGit(t, worktree, \"rev-parse\", \"HEAD\")\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: false, Interval: 2}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\n\t\tif gotReq.Commit == \"\" {\n\t\t\tt.Fatal(\"expected build creation request with a commit, got empty\")\n\t\t}\n\t\tif !strings.HasPrefix(gotReq.Branch, \"bk/preflight/\") {\n\t\t\tt.Errorf(\"expected branch starting with bk/preflight/, got %q\", gotReq.Branch)\n\t\t}\n\t\tif !strings.HasPrefix(gotReq.Message, \"Preflight \") {\n\t\t\tt.Errorf(\"expected message starting with 'Preflight ', got %q\", gotReq.Message)\n\t\t}\n\t\tif gotReq.Env[\"PREFLIGHT\"] != \"true\" {\n\t\t\tt.Errorf(\"expected PREFLIGHT=true, got %#v\", gotReq.Env)\n\t\t}\n\t\tif gotReq.Env[\"BUILDKITE_PREFLIGHT\"] != \"true\" {\n\t\t\tt.Errorf(\"expected BUILDKITE_PREFLIGHT=true (deprecated), got %#v\", gotReq.Env)\n\t\t}\n\t\tif gotReq.Env[\"PREFLIGHT_SOURCE_BRANCH\"] != expectedSourceBranch {\n\t\t\tt.Errorf(\"expected PREFLIGHT_SOURCE_BRANCH=%q, got %#v\", expectedSourceBranch, gotReq.Env)\n\t\t}\n\t\tif gotReq.Env[\"PREFLIGHT_SOURCE_COMMIT\"] != expectedSourceCommit {\n\t\t\tt.Errorf(\"expected PREFLIGHT_SOURCE_COMMIT=%q, got %#v\", expectedSourceCommit, gotReq.Env)\n\t\t}\n\t\tif !strings.Contains(gotUserAgent, buildkite.DefaultUserAgent) {\n\t\t\tt.Errorf(\"expected User-Agent to contain %q, got %q\", buildkite.DefaultUserAgent, gotUserAgent)\n\t\t}\n\t\tif !strings.Contains(gotUserAgent, \"buildkite-cli-preflight/\") {\n\t\t\tt.Errorf(\"expected User-Agent to contain preflight token, got %q\", gotUserAgent)\n\t\t}\n\t})\n\n\tt.Run(\"omits source branch env when git HEAD is detached\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar gotReq buildkite.CreateBuild\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method == \"POST\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tjson.NewDecoder(r.Body).Decode(&gotReq)\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\texpectedSourceCommit := runGit(t, worktree, \"rev-parse\", \"HEAD\")\n\t\trunGit(t, worktree, \"checkout\", expectedSourceCommit)\n\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: false, Interval: 2}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\n\t\tif _, ok := gotReq.Env[\"PREFLIGHT_SOURCE_BRANCH\"]; ok {\n\t\t\tt.Errorf(\"expected PREFLIGHT_SOURCE_BRANCH to be omitted in detached HEAD, got %#v\", gotReq.Env)\n\t\t}\n\t\tif gotReq.Env[\"PREFLIGHT_SOURCE_COMMIT\"] != expectedSourceCommit {\n\t\t\tt.Errorf(\"expected PREFLIGHT_SOURCE_COMMIT=%q, got %#v\", expectedSourceCommit, gotReq.Env)\n\t\t}\n\t})\n\n\tt.Run(\"falls back to git cli when factory cannot open repository\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\toriginalNewFactory := newFactory\n\t\tt.Cleanup(func() { newFactory = originalNewFactory })\n\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tswitch {\n\t\t\tcase r.Method == \"POST\" && strings.Contains(r.URL.Path, \"/builds\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\tcase r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tNumber:     1,\n\t\t\t\t\tState:      \"passed\",\n\t\t\t\t\tFinishedAt: &buildkite.Timestamp{Time: now},\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tnewFactory = func(...factory.FactoryOpt) (*factory.Factory, error) {\n\t\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn &factory.Factory{\n\t\t\t\tConfig:        config.New(nil, nil),\n\t\t\t\tRestAPIClient: client,\n\t\t\t}, nil\n\t\t}\n\n\t\tworktree := initTestRepo(t)\n\t\tsubdir := filepath.Join(worktree, \"nested\", \"dir\")\n\t\tif err := os.MkdirAll(subdir, 0o755); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tt.Chdir(subdir)\n\t\tif err := os.WriteFile(filepath.Join(subdir, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01}\n\t\tif err := cmd.Run(nil, stubGlobals{}); err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif strings.Contains(refs, \"bk/preflight/\") {\n\t\t\tt.Errorf(\"expected preflight branch to be cleaned up, but found: %s\", refs)\n\t\t}\n\t})\n\n\tt.Run(\"watches build until completion and cleans up remote branch\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tpollCount := 0\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tif r.Method == \"POST\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds/1\") {\n\t\t\t\tpollCount++\n\t\t\t\tb := buildkite.Build{Number: 1, State: \"running\"}\n\t\t\t\tif pollCount >= 3 {\n\t\t\t\t\tb.State = \"passed\"\n\t\t\t\t\tb.FinishedAt = &buildkite.Timestamp{Time: now}\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(b)\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\t\tif pollCount < 3 {\n\t\t\tt.Errorf(\"expected at least 3 polls, got %d\", pollCount)\n\t\t}\n\n\t\t// Verify the remote preflight branch was deleted.\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif strings.Contains(refs, \"bk/preflight/\") {\n\t\t\tt.Errorf(\"expected preflight branch to be cleaned up, but found: %s\", refs)\n\t\t}\n\t})\n\n\tt.Run(\"early exit summary tolerates summary endpoint failure\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar buildCancelRequests atomic.Int32\n\t\tvar buildPolls atomic.Int32\n\t\tvar summaryRequests atomic.Int32\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodPut && strings.Contains(r.URL.Path, \"/builds/1/cancel\"):\n\t\t\t\tbuildCancelRequests.Add(1)\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{Number: 1, State: \"canceling\"})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodPost && strings.Contains(r.URL.Path, \"/builds\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\tpoll := buildPolls.Add(1)\n\t\t\t\tbuild := buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"running\",\n\t\t\t\t\tJobs: []buildkite.Job{{\n\t\t\t\t\t\tID:    \"job-running\",\n\t\t\t\t\t\tType:  \"script\",\n\t\t\t\t\t\tName:  \"Lint\",\n\t\t\t\t\t\tState: \"running\",\n\t\t\t\t\t}},\n\t\t\t\t}\n\t\t\t\tif poll >= 2 {\n\t\t\t\t\texitOne := 1\n\t\t\t\t\tbuild.State = \"failing\"\n\t\t\t\t\tbuild.Jobs = []buildkite.Job{{\n\t\t\t\t\t\tID:         \"job-failed\",\n\t\t\t\t\t\tType:       \"script\",\n\t\t\t\t\t\tName:       \"Lint\",\n\t\t\t\t\t\tState:      \"failed\",\n\t\t\t\t\t\tExitStatus: &exitOne,\n\t\t\t\t\t}}\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(build)\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/tests\"):\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.BuildTest{})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/v2/analytics/organizations/test-org/builds/build-id-123/preflight/v1\":\n\t\t\t\tsummaryRequests.Add(1)\n\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\t\t_, _ = w.Write([]byte(`{\"message\":\"API::Error::NotFound\"}`))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tstdout := captureStdout(t, func() {\n\t\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01, JSON: true}\n\t\t\terr := cmd.Run(nil, stubGlobals{})\n\t\t\tvar bkErr *bkErrors.Error\n\t\t\tif !errors.As(err, &bkErr) || !errors.Is(bkErr, bkErrors.ErrPreflightIncompleteFailure) {\n\t\t\t\tt.Fatalf(\"expected incomplete failure error, got %v\", err)\n\t\t\t}\n\t\t})\n\n\t\tevents := decodeJSONLEvents(t, stdout)\n\t\tvar buildStatusCount int\n\t\tvar summaries []Event\n\t\tfor _, event := range events {\n\t\t\tif event.Type == EventBuildStatus {\n\t\t\t\tbuildStatusCount++\n\t\t\t}\n\t\t\tif event.Type == EventBuildSummary {\n\t\t\t\tsummaries = append(summaries, event)\n\t\t\t}\n\t\t}\n\n\t\tif buildStatusCount != 2 {\n\t\t\tt.Fatalf(\"expected 2 build status events before early stop, got %d\", buildStatusCount)\n\t\t}\n\t\tif len(summaries) != 1 {\n\t\t\tt.Fatalf(\"expected exactly 1 build summary event, got %d\", len(summaries))\n\t\t}\n\t\tsummary := summaries[0]\n\t\tif !summary.Incomplete {\n\t\t\tt.Fatal(\"expected summary to be marked incomplete\")\n\t\t}\n\t\tif summary.StopReason != \"build-failing\" {\n\t\t\tt.Fatalf(\"expected stop reason build-failing, got %q\", summary.StopReason)\n\t\t}\n\t\tif summary.BuildCanceled == nil || !*summary.BuildCanceled {\n\t\t\tt.Fatalf(\"expected build_canceled=true, got %#v\", summary.BuildCanceled)\n\t\t}\n\t\tif summary.BuildState != \"failing\" {\n\t\t\tt.Fatalf(\"expected failing build state, got %q\", summary.BuildState)\n\t\t}\n\t\tif len(summary.FailedJobs) != 1 || summary.FailedJobs[0].Name != \"Lint\" {\n\t\t\tt.Fatalf(\"expected failed jobs in summary, got %#v\", summary.FailedJobs)\n\t\t}\n\t\tif len(summary.Tests.Runs) != 0 || len(summary.Tests.Failures) != 0 {\n\t\t\tt.Fatalf(\"expected no enriched tests when summary endpoint fails, got %#v\", summary.Tests)\n\t\t}\n\t\tif summaryRequests.Load() != 1 {\n\t\t\tt.Fatalf(\"expected one preflight summary request, got %d\", summaryRequests.Load())\n\t\t}\n\t\tif buildCancelRequests.Load() != 1 {\n\t\t\tt.Fatalf(\"expected one build cancel request, got %d\", buildCancelRequests.Load())\n\t\t}\n\t\tif buildPolls.Load() != 3 {\n\t\t\tt.Fatalf(\"expected three build polls including final summary fetch, got %d\", buildPolls.Load())\n\t\t}\n\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif strings.Contains(refs, \"bk/preflight/\") {\n\t\t\tt.Errorf(\"expected preflight branch to be cleaned up, but found: %s\", refs)\n\t\t}\n\t})\n\n\tt.Run(\"no-cleanup leaves branch and build running after early stop\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar buildCancelRequests atomic.Int32\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodPut && strings.Contains(r.URL.Path, \"/builds/1/cancel\"):\n\t\t\t\tbuildCancelRequests.Add(1)\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{Number: 1, State: \"canceling\"})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodPost && strings.Contains(r.URL.Path, \"/builds\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\texitOne := 1\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"failing\",\n\t\t\t\t\tJobs: []buildkite.Job{{\n\t\t\t\t\t\tID:         \"job-failed\",\n\t\t\t\t\t\tType:       \"script\",\n\t\t\t\t\t\tName:       \"Lint\",\n\t\t\t\t\t\tState:      \"failed\",\n\t\t\t\t\t\tExitStatus: &exitOne,\n\t\t\t\t\t}},\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/tests\"):\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.BuildTest{})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tstdout := captureStdout(t, func() {\n\t\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01, JSON: true, NoCleanup: true}\n\t\t\terr := cmd.Run(nil, stubGlobals{})\n\t\t\tvar bkErr *bkErrors.Error\n\t\t\tif !errors.As(err, &bkErr) || !errors.Is(bkErr, bkErrors.ErrPreflightIncompleteFailure) {\n\t\t\t\tt.Fatalf(\"expected incomplete failure error, got %v\", err)\n\t\t\t}\n\t\t})\n\n\t\tevents := decodeJSONLEvents(t, stdout)\n\t\tvar summaries []Event\n\t\tfor _, event := range events {\n\t\t\tif event.Type == EventBuildSummary {\n\t\t\t\tsummaries = append(summaries, event)\n\t\t\t}\n\t\t}\n\n\t\tif len(summaries) != 1 {\n\t\t\tt.Fatalf(\"expected exactly 1 build summary event, got %d\", len(summaries))\n\t\t}\n\t\tsummary := summaries[0]\n\t\tif summary.BuildCanceled == nil || *summary.BuildCanceled {\n\t\t\tt.Fatalf(\"expected build_canceled=false, got %#v\", summary.BuildCanceled)\n\t\t}\n\t\tif buildCancelRequests.Load() != 0 {\n\t\t\tt.Fatalf(\"expected no build cancel requests, got %d\", buildCancelRequests.Load())\n\t\t}\n\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif !strings.Contains(refs, \"bk/preflight/\") {\n\t\t\tt.Error(\"expected preflight branch to still exist with --no-cleanup\")\n\t\t}\n\t})\n\n\tt.Run(\"build-terminal waits for terminal completion\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar buildCancelRequests atomic.Int32\n\t\tvar buildPolls atomic.Int32\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodPut && strings.Contains(r.URL.Path, \"/builds/1/cancel\"):\n\t\t\t\tbuildCancelRequests.Add(1)\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{Number: 1, State: \"canceling\"})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodPost && strings.Contains(r.URL.Path, \"/builds\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\tpoll := buildPolls.Add(1)\n\t\t\t\texitOne := 1\n\t\t\t\tbuild := buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"running\",\n\t\t\t\t\tJobs: []buildkite.Job{{\n\t\t\t\t\t\tID:    \"job-running\",\n\t\t\t\t\t\tType:  \"script\",\n\t\t\t\t\t\tName:  \"Lint\",\n\t\t\t\t\t\tState: \"running\",\n\t\t\t\t\t}},\n\t\t\t\t}\n\t\t\t\tif poll >= 2 {\n\t\t\t\t\tbuild.State = \"failing\"\n\t\t\t\t\tbuild.Jobs = []buildkite.Job{{\n\t\t\t\t\t\tID:         \"job-failed\",\n\t\t\t\t\t\tType:       \"script\",\n\t\t\t\t\t\tName:       \"Lint\",\n\t\t\t\t\t\tState:      \"failed\",\n\t\t\t\t\t\tExitStatus: &exitOne,\n\t\t\t\t\t}}\n\t\t\t\t}\n\t\t\t\tif poll >= 3 {\n\t\t\t\t\tbuild.State = \"failed\"\n\t\t\t\t\tbuild.FinishedAt = &buildkite.Timestamp{Time: now}\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(build)\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/tests\"):\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.BuildTest{})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tstdout := captureStdout(t, func() {\n\t\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01, JSON: true, ExitOn: []internalpreflight.ExitPolicy{internalpreflight.ExitOnBuildTerminal}}\n\t\t\terr := cmd.Run(nil, stubGlobals{})\n\t\t\tvar bkErr *bkErrors.Error\n\t\t\tif !errors.As(err, &bkErr) || !errors.Is(bkErr, bkErrors.ErrPreflightCompletedFailure) {\n\t\t\t\tt.Fatalf(\"expected completed failure error, got %v\", err)\n\t\t\t}\n\t\t})\n\n\t\tevents := decodeJSONLEvents(t, stdout)\n\t\tvar buildStatusCount int\n\t\tvar summaries []Event\n\t\tfor _, event := range events {\n\t\t\tif event.Type == EventBuildStatus {\n\t\t\t\tbuildStatusCount++\n\t\t\t}\n\t\t\tif event.Type == EventBuildSummary {\n\t\t\t\tsummaries = append(summaries, event)\n\t\t\t}\n\t\t}\n\n\t\tif buildStatusCount != 3 {\n\t\t\tt.Fatalf(\"expected 3 build status events before terminal exit, got %d\", buildStatusCount)\n\t\t}\n\t\tif len(summaries) != 1 {\n\t\t\tt.Fatalf(\"expected exactly 1 build summary event, got %d\", len(summaries))\n\t\t}\n\t\tsummary := summaries[0]\n\t\tif summary.Incomplete {\n\t\t\tt.Fatal(\"expected terminal summary, got incomplete=true\")\n\t\t}\n\t\tif summary.StopReason != \"\" {\n\t\t\tt.Fatalf(\"expected empty stop reason, got %q\", summary.StopReason)\n\t\t}\n\t\tif summary.BuildCanceled != nil {\n\t\t\tt.Fatalf(\"expected no build_canceled metadata for terminal summary, got %#v\", summary.BuildCanceled)\n\t\t}\n\t\tif summary.BuildState != \"failed\" {\n\t\t\tt.Fatalf(\"expected failed build state, got %q\", summary.BuildState)\n\t\t}\n\t\tif buildCancelRequests.Load() != 0 {\n\t\t\tt.Fatalf(\"expected no build cancel requests, got %d\", buildCancelRequests.Load())\n\t\t}\n\t\tif buildPolls.Load() < 3 {\n\t\t\tt.Fatalf(\"expected to keep polling through terminal state, got %d polls\", buildPolls.Load())\n\t\t}\n\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif strings.Contains(refs, \"bk/preflight/\") {\n\t\t\tt.Errorf(\"expected preflight branch to be cleaned up, but found: %s\", refs)\n\t\t}\n\t})\n\n\tt.Run(\"final summary does not retry test results without await flag\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar includeLatestFail atomic.Bool\n\t\tvar summaryRequests atomic.Int32\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodPost && strings.Contains(r.URL.Path, \"/builds\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:         \"build-id-123\",\n\t\t\t\t\tNumber:     1,\n\t\t\t\t\tState:      \"failed\",\n\t\t\t\t\tWebURL:     \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t\tFinishedAt: &buildkite.Timestamp{Time: now},\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t\tTestEngine: &buildkite.TestEngineProperty{\n\t\t\t\t\t\tRuns: []buildkite.TestEngineRun{{\n\t\t\t\t\t\t\tID: \"run-1\",\n\t\t\t\t\t\t\tSuite: buildkite.TestEngineSuite{\n\t\t\t\t\t\t\t\tSlug: \"rspec\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t\tJobs: []buildkite.Job{{\n\t\t\t\t\t\tID:    \"job-failed\",\n\t\t\t\t\t\tType:  \"script\",\n\t\t\t\t\t\tName:  \"RSpec shard 1\",\n\t\t\t\t\t\tState: \"failed\",\n\t\t\t\t\t}},\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/tests\"):\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.BuildTest{})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/v2/organizations/test-org/builds\":\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.Build{{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t}})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/v2/analytics/organizations/test-org/builds/build-id-123/preflight/v1\":\n\t\t\t\tsummaryRequests.Add(1)\n\t\t\t\tif r.URL.Query().Get(\"include\") == \"latest_fail\" {\n\t\t\t\t\tincludeLatestFail.Store(true)\n\t\t\t\t}\n\t\t\t\tif summaryRequests.Load() == 1 {\n\t\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\t\t\t_, _ = w.Write([]byte(`{\"message\":\"API::Error::NotFound\"}`))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t_, _ = w.Write([]byte(`{\n\t\t\t\t\t\"tests\": {\n\t\t\t\t\t\t\"runs\": {\n\t\t\t\t\t\t\t\"run-1\": {\n\t\t\t\t\t\t\t\t\"suite\": {\"id\": \"suite-1\", \"slug\": \"rspec\", \"name\": \"RSpec\"},\n\t\t\t\t\t\t\t\t\"passed\": 47,\n\t\t\t\t\t\t\t\t\"failed\": 1,\n\t\t\t\t\t\t\t\t\"skipped\": 12\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"failures\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"run_id\": \"run-1\",\n\t\t\t\t\t\t\t\t\"suite_name\": \"RSpec\",\n\t\t\t\t\t\t\t\t\"suite_slug\": \"rspec\",\n\t\t\t\t\t\t\t\t\"name\": \"AuthService.validateToken handles expired tokens\",\n\t\t\t\t\t\t\t\t\"location\": \"src/auth.test.ts:89\",\n\t\t\t\t\t\t\t\t\"latest_fail\": {\n\t\t\t\t\t\t\t\t\t\"failure_reason\": \"Expected 'expired' but got 'invalid'\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}`))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tstdout := captureStdout(t, func() {\n\t\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01, Text: true}\n\t\t\terr := cmd.Run(nil, stubGlobals{})\n\t\t\tvar bkErr *bkErrors.Error\n\t\t\tif !errors.As(err, &bkErr) || !errors.Is(bkErr, bkErrors.ErrPreflightCompletedFailure) {\n\t\t\t\tt.Fatalf(\"expected completed failure error, got %v\", err)\n\t\t\t}\n\t\t})\n\n\t\tif !includeLatestFail.Load() {\n\t\t\tt.Fatal(\"expected preflight summary to request latest_fail details\")\n\t\t}\n\t\tif got := summaryRequests.Load(); got != 1 {\n\t\t\tt.Fatalf(\"expected one summary request without await flag, got %d\", got)\n\t\t}\n\t\tif strings.Contains(stdout, \"AuthService.validateToken handles expired tokens\") {\n\t\t\tt.Fatalf(\"expected no endpoint failure name in final summary, got %q\", stdout)\n\t\t}\n\t\tif strings.Contains(stdout, \"Expected 'expired' but got 'invalid'\") {\n\t\t\tt.Fatalf(\"expected no endpoint failure message in final summary, got %q\", stdout)\n\t\t}\n\t})\n\n\tt.Run(\"final summary tolerates transient build lookup failure without await flag\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar buildRequests atomic.Int32\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodPost && strings.Contains(r.URL.Path, \"/builds\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\tif buildRequests.Add(1) == 1 {\n\t\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\t\tID:         \"build-id-123\",\n\t\t\t\t\t\tNumber:     1,\n\t\t\t\t\t\tState:      \"failed\",\n\t\t\t\t\t\tWebURL:     \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t\t\tFinishedAt: &buildkite.Timestamp{Time: now},\n\t\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tJobs: []buildkite.Job{{\n\t\t\t\t\t\t\tID:    \"job-failed\",\n\t\t\t\t\t\t\tType:  \"script\",\n\t\t\t\t\t\t\tName:  \"RSpec shard 1\",\n\t\t\t\t\t\t\tState: \"failed\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\t_, _ = w.Write([]byte(`{\"message\":\"temporary failure\"}`))\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/tests\"):\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.BuildTest{})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/v2/organizations/test-org/builds\":\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.Build{{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t}})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tstdout := captureStdout(t, func() {\n\t\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01, Text: true}\n\t\t\terr := cmd.Run(nil, stubGlobals{})\n\t\t\tvar bkErr *bkErrors.Error\n\t\t\tif !errors.As(err, &bkErr) || !errors.Is(bkErr, bkErrors.ErrPreflightCompletedFailure) {\n\t\t\t\tt.Fatalf(\"expected completed failure error, got %v\", err)\n\t\t\t}\n\t\t})\n\n\t\tif !strings.Contains(stdout, \"❌ Preflight Failed\") {\n\t\t\tt.Fatalf(\"expected final summary header, got %q\", stdout)\n\t\t}\n\t\tif !strings.Contains(stdout, \"RSpec shard 1\") {\n\t\t\tt.Fatalf(\"expected failed job in final summary, got %q\", stdout)\n\t\t}\n\t})\n\n\tt.Run(\"await-test-results loads summary after timeout\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar includeLatestFail atomic.Bool\n\t\tvar stateEnabled atomic.Bool\n\t\tvar summaryRequests atomic.Int32\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodPost && strings.Contains(r.URL.Path, \"/builds\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:         \"build-id-123\",\n\t\t\t\t\tNumber:     1,\n\t\t\t\t\tState:      \"failed\",\n\t\t\t\t\tWebURL:     \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t\tFinishedAt: &buildkite.Timestamp{Time: now},\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t\tTestEngine: &buildkite.TestEngineProperty{\n\t\t\t\t\t\tRuns: []buildkite.TestEngineRun{{\n\t\t\t\t\t\t\tID: \"run-1\",\n\t\t\t\t\t\t\tSuite: buildkite.TestEngineSuite{\n\t\t\t\t\t\t\t\tSlug: \"rspec\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t\tJobs: []buildkite.Job{{\n\t\t\t\t\t\tID:    \"job-failed\",\n\t\t\t\t\t\tType:  \"script\",\n\t\t\t\t\t\tName:  \"RSpec shard 1\",\n\t\t\t\t\t\tState: \"failed\",\n\t\t\t\t\t}},\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/tests\"):\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.BuildTest{})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/v2/organizations/test-org/builds\":\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.Build{{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t}})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/v2/analytics/organizations/test-org/builds/build-id-123/preflight/v1\":\n\t\t\t\tsummaryRequests.Add(1)\n\t\t\t\tif r.URL.Query().Get(\"include\") == \"latest_fail\" {\n\t\t\t\t\tincludeLatestFail.Store(true)\n\t\t\t\t}\n\t\t\t\tif r.URL.Query().Get(\"state\") == \"enabled\" {\n\t\t\t\t\tstateEnabled.Store(true)\n\t\t\t\t}\n\t\t\t\t_, _ = w.Write([]byte(`{\n\t\t\t\t\t\t\"tests\": {\n\t\t\t\t\t\t\"runs\": {\n\t\t\t\t\t\t\t\"run-1\": {\n\t\t\t\t\t\t\t\t\"suite\": {\"id\": \"suite-1\", \"slug\": \"rspec\", \"name\": \"RSpec\"},\n\t\t\t\t\t\t\t\t\"passed\": 47,\n\t\t\t\t\t\t\t\t\"failed\": 1,\n\t\t\t\t\t\t\t\t\"skipped\": 12\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"failures\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"run_id\": \"run-1\",\n\t\t\t\t\t\t\t\t\"suite_name\": \"RSpec\",\n\t\t\t\t\t\t\t\t\"suite_slug\": \"rspec\",\n\t\t\t\t\t\t\t\t\"name\": \"AuthService.validateToken handles expired tokens\",\n\t\t\t\t\t\t\t\t\"location\": \"src/auth.test.ts:89\",\n\t\t\t\t\t\t\t\t\"latest_fail\": {\n\t\t\t\t\t\t\t\t\t\"failure_reason\": \"Expected 'expired' but got 'invalid'\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}`))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tstdout := captureStdout(t, func() {\n\t\t\tcmd := &RunCmd{\n\t\t\t\tPipeline:         \"test-org/test-pipeline\",\n\t\t\t\tWatch:            true,\n\t\t\t\tInterval:         0.01,\n\t\t\t\tText:             true,\n\t\t\t\tAwaitTestResults: awaitTestResultsFlag{Enabled: true, Duration: 35 * time.Millisecond},\n\t\t\t}\n\t\t\terr := cmd.Run(nil, stubGlobals{})\n\t\t\tvar bkErr *bkErrors.Error\n\t\t\tif !errors.As(err, &bkErr) || !errors.Is(bkErr, bkErrors.ErrPreflightCompletedFailure) {\n\t\t\t\tt.Fatalf(\"expected completed failure error, got %v\", err)\n\t\t\t}\n\t\t})\n\n\t\tif !includeLatestFail.Load() {\n\t\t\tt.Fatal(\"expected preflight summary to request latest_fail details\")\n\t\t}\n\t\tif !stateEnabled.Load() {\n\t\t\tt.Fatal(\"expected preflight summary to request state=enabled\")\n\t\t}\n\t\tif got := summaryRequests.Load(); got != 1 {\n\t\t\tt.Fatalf(\"expected one delayed summary request, got %d\", got)\n\t\t}\n\t\tif !strings.Contains(stdout, \"✗ RSpec  1 failed  47 passed  12 skipped\") {\n\t\t\tt.Fatalf(\"expected suite name in final summary, got %q\", stdout)\n\t\t}\n\t\tif !strings.Contains(stdout, \"✗ [RSpec]\") {\n\t\t\tt.Fatalf(\"expected suite name in failure label, got %q\", stdout)\n\t\t}\n\t\tif !strings.Contains(stdout, \"AuthService.validateToken handles expired tokens\") {\n\t\t\tt.Fatalf(\"expected endpoint failure name in final summary, got %q\", stdout)\n\t\t}\n\t\tif strings.Contains(stdout, \"Expected 'expired' but got 'invalid'\") {\n\t\t\tt.Fatalf(\"expected final summary to omit endpoint failure message, got %q\", stdout)\n\t\t}\n\t})\n\n\tt.Run(\"await-test-results timeout still renders final summary\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar summaryRequests atomic.Int32\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodPost && strings.Contains(r.URL.Path, \"/builds\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:         \"build-id-123\",\n\t\t\t\t\tNumber:     1,\n\t\t\t\t\tState:      \"failed\",\n\t\t\t\t\tWebURL:     \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t\tFinishedAt: &buildkite.Timestamp{Time: now},\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t\tTestEngine: &buildkite.TestEngineProperty{\n\t\t\t\t\t\tRuns: []buildkite.TestEngineRun{{\n\t\t\t\t\t\t\tID: \"run-1\",\n\t\t\t\t\t\t\tSuite: buildkite.TestEngineSuite{\n\t\t\t\t\t\t\t\tSlug: \"rspec\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t\tJobs: []buildkite.Job{{\n\t\t\t\t\t\tID:    \"job-failed\",\n\t\t\t\t\t\tType:  \"script\",\n\t\t\t\t\t\tName:  \"RSpec shard 1\",\n\t\t\t\t\t\tState: \"failed\",\n\t\t\t\t\t}},\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/tests\"):\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.BuildTest{})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/v2/organizations/test-org/builds\":\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.Build{{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t}})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/v2/analytics/organizations/test-org/builds/build-id-123/preflight/v1\":\n\t\t\t\tsummaryRequests.Add(1)\n\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\t\t_, _ = w.Write([]byte(`{\"message\":\"API::Error::NotFound\"}`))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tstdout := captureStdout(t, func() {\n\t\t\tcmd := &RunCmd{\n\t\t\t\tPipeline:         \"test-org/test-pipeline\",\n\t\t\t\tWatch:            true,\n\t\t\t\tInterval:         0.01,\n\t\t\t\tText:             true,\n\t\t\t\tAwaitTestResults: awaitTestResultsFlag{Enabled: true, Duration: 35 * time.Millisecond},\n\t\t\t}\n\t\t\terr := cmd.Run(nil, stubGlobals{})\n\t\t\tvar bkErr *bkErrors.Error\n\t\t\tif !errors.As(err, &bkErr) || !errors.Is(bkErr, bkErrors.ErrPreflightCompletedFailure) {\n\t\t\t\tt.Fatalf(\"expected completed failure error, got %v\", err)\n\t\t\t}\n\t\t})\n\n\t\tif got := summaryRequests.Load(); got != 1 {\n\t\t\tt.Fatalf(\"expected one delayed summary request during await timeout, got %d\", got)\n\t\t}\n\t\tif !strings.Contains(stdout, \"❌ Preflight Failed\") {\n\t\t\tt.Fatalf(\"expected final summary header, got %q\", stdout)\n\t\t}\n\t\tif strings.Contains(stdout, \"AuthService.validateToken handles expired tokens\") {\n\t\t\tt.Fatalf(\"expected no endpoint failure name in final summary, got %q\", stdout)\n\t\t}\n\t})\n\n\tt.Run(\"await-test-results does not wait when no test runs are expected\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar summaryRequests atomic.Int32\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodPost && strings.Contains(r.URL.Path, \"/builds\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tID:         \"build-id-123\",\n\t\t\t\t\tNumber:     1,\n\t\t\t\t\tState:      \"failed\",\n\t\t\t\t\tWebURL:     \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t\tFinishedAt: &buildkite.Timestamp{Time: now},\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t\tJobs: []buildkite.Job{{\n\t\t\t\t\t\tID:    \"job-failed\",\n\t\t\t\t\t\tType:  \"script\",\n\t\t\t\t\t\tName:  \"RSpec shard 1\",\n\t\t\t\t\t\tState: \"failed\",\n\t\t\t\t\t}},\n\t\t\t\t})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && strings.Contains(r.URL.Path, \"/tests\"):\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.BuildTest{})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/v2/organizations/test-org/builds\":\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.Build{{\n\t\t\t\t\tID:     \"build-id-123\",\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tPipeline: &buildkite.Pipeline{\n\t\t\t\t\t\tSlug: \"test-pipeline\",\n\t\t\t\t\t},\n\t\t\t\t}})\n\t\t\t\treturn\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/v2/analytics/organizations/test-org/builds/build-id-123/preflight/v1\":\n\t\t\t\tsummaryRequests.Add(1)\n\t\t\t\t_, _ = w.Write([]byte(`{\"tests\":{\"runs\":{},\"failures\":[]}}`))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tcmd := &RunCmd{\n\t\t\tPipeline:         \"test-org/test-pipeline\",\n\t\t\tWatch:            true,\n\t\t\tInterval:         0.01,\n\t\t\tAwaitTestResults: awaitTestResultsFlag{Enabled: true, Duration: 35 * time.Millisecond},\n\t\t}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tvar bkErr *bkErrors.Error\n\t\tif !errors.As(err, &bkErr) || !errors.Is(bkErr, bkErrors.ErrPreflightCompletedFailure) {\n\t\t\tt.Fatalf(\"expected completed failure error, got %v\", err)\n\t\t}\n\t\tif got := summaryRequests.Load(); got != 0 {\n\t\t\tt.Fatalf(\"expected no summary requests when no test runs are expected, got %d\", got)\n\t\t}\n\t})\n\n\tt.Run(\"no-cleanup preserves remote branch\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tif r.Method == \"POST\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds/1\") {\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tNumber:     1,\n\t\t\t\t\tState:      \"passed\",\n\t\t\t\t\tFinishedAt: &buildkite.Timestamp{Time: now},\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01, NoCleanup: true}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error, got: %v\", err)\n\t\t}\n\n\t\t// Verify the remote preflight branch still exists.\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif !strings.Contains(refs, \"bk/preflight/\") {\n\t\t\tt.Error(\"expected preflight branch to still exist with --no-cleanup\")\n\t\t}\n\t})\n\n\tt.Run(\"returns error when build fails\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tif r.Method == \"POST\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds/1\") {\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tNumber:     1,\n\t\t\t\t\tState:      \"failed\",\n\t\t\t\t\tFinishedAt: &buildkite.Timestamp{Time: now},\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t}\n\t\tif !strings.Contains(err.Error(), \"preflight completed with failure: build is failed\") {\n\t\t\tt.Errorf(\"expected completed failure error, got: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"returns user aborted error when interrupted while watching\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\toriginalNotifyContext := notifyContext\n\t\tt.Cleanup(func() { notifyContext = originalNotifyContext })\n\n\t\twatchCtx, cancelWatch := context.WithCancel(context.Background())\n\t\tnotifyContext = func(context.Context, ...os.Signal) (context.Context, context.CancelFunc) {\n\t\t\treturn watchCtx, cancelWatch\n\t\t}\n\n\t\tvar buildCancelRequests atomic.Int32\n\t\tvar pollCount atomic.Int32\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tswitch {\n\t\t\tcase r.Method == \"PUT\" && strings.Contains(r.URL.Path, \"/builds/1/cancel\"):\n\t\t\t\tbuildCancelRequests.Add(1)\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{Number: 1, State: \"canceling\"})\n\t\t\t\treturn\n\t\t\tcase r.Method == \"POST\" && strings.Contains(r.URL.Path, \"/builds\"):\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\tcase r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\tif pollCount.Add(1) == 1 {\n\t\t\t\t\tcancelWatch()\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{Number: 1, State: \"running\"})\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t}\n\t\tif !bkErrors.IsUserAborted(err) {\n\t\t\tt.Fatalf(\"expected user aborted error, got %T: %v\", err, err)\n\t\t}\n\t\tif code := bkErrors.GetExitCodeForError(err); code != bkErrors.ExitCodeUserAbortedError {\n\t\t\tt.Fatalf(\"expected exit code %d, got %d\", bkErrors.ExitCodeUserAbortedError, code)\n\t\t}\n\t\tif pollCount.Load() == 0 {\n\t\t\tt.Fatal(\"expected at least one build poll before interrupt\")\n\t\t}\n\t\tif buildCancelRequests.Load() != 1 {\n\t\t\tt.Fatalf(\"expected one build cancel request, got %d\", buildCancelRequests.Load())\n\t\t}\n\t})\n\n\tt.Run(\"aborts after 10 consecutive polling errors\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tpollCount := 0\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tif r.Method == \"POST\" && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\t\tNumber: 1,\n\t\t\t\t\tState:  \"scheduled\",\n\t\t\t\t\tWebURL: \"https://buildkite.com/test-org/test-pipeline/builds/1\",\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds/1\") {\n\t\t\t\tpollCount++\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Watch: true, Interval: 0.01}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t}\n\t\tif !strings.Contains(err.Error(), \"watching build failed\") {\n\t\t\tt.Errorf(\"expected 'watching build failed', got: %v\", err)\n\t\t}\n\t\tif pollCount < watch.DefaultMaxConsecutiveErrors {\n\t\t\tt.Errorf(\"expected at least %d polls, got %d\", watch.DefaultMaxConsecutiveErrors, pollCount)\n\t\t}\n\t})\n\n\tt.Run(\"returns error when build creation fails\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.WriteHeader(http.StatusUnprocessableEntity)\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tw.Write([]byte(`{\"message\":\"Pipeline not found\"}`))\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Interval: 2}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t}\n\t\tif !strings.Contains(err.Error(), \"creating preflight build\") {\n\t\t\tt.Fatalf(\"expected build creation error, got: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"returns user aborted when canceled during snapshot push\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar cancel context.CancelFunc\n\t\toriginalNotifyContext := notifyContext\n\t\tnotifyContext = func(parent context.Context, signals ...os.Signal) (context.Context, context.CancelFunc) {\n\t\t\tctx, stop := context.WithCancel(parent)\n\t\t\tcancel = stop\n\t\t\treturn ctx, stop\n\t\t}\n\t\tt.Cleanup(func() { notifyContext = originalNotifyContext })\n\n\t\tvar apiRequests atomic.Int32\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tapiRequests.Add(1)\n\t\t\thttp.Error(w, \"unexpected request after snapshot cancellation\", http.StatusInternalServerError)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tgitPath, err := exec.LookPath(\"git\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"finding git: %v\", err)\n\t\t}\n\n\t\tfakeBin := t.TempDir()\n\t\tpushStarted := filepath.Join(fakeBin, \"push-started\")\n\t\tfakeGit := filepath.Join(fakeBin, \"git\")\n\t\tif err := os.WriteFile(fakeGit, []byte(`#!/bin/sh\nif [ \"$1\" = \"push\" ]; then\n\ttouch \"$PUSH_STARTED\"\n\texec /bin/sleep 10\nfi\nexec \"$REAL_GIT\" \"$@\"\n`), 0o755); err != nil {\n\t\t\tt.Fatalf(\"writing fake git: %v\", err)\n\t\t}\n\t\tt.Setenv(\"REAL_GIT\", gitPath)\n\t\tt.Setenv(\"PUSH_STARTED\", pushStarted)\n\t\tt.Setenv(\"PATH\", fakeBin+string(os.PathListSeparator)+os.Getenv(\"PATH\"))\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Interval: 2}\n\t\terrCh := make(chan error, 1)\n\t\tgo func() { errCh <- cmd.Run(nil, stubGlobals{}) }()\n\n\t\tdeadline := time.After(2 * time.Second)\n\t\tfor {\n\t\t\tif _, err := os.Stat(pushStarted); err == nil {\n\t\t\t\tbreak\n\t\t\t} else if !os.IsNotExist(err) {\n\t\t\t\tt.Fatalf(\"checking push marker: %v\", err)\n\t\t\t}\n\n\t\t\tselect {\n\t\t\tcase err := <-errCh:\n\t\t\t\tt.Fatalf(\"Run returned before snapshot push was canceled: %v\", err)\n\t\t\tcase <-deadline:\n\t\t\t\tt.Fatal(\"timed out waiting for snapshot push to start\")\n\t\t\tcase <-time.After(10 * time.Millisecond):\n\t\t\t}\n\t\t}\n\n\t\tcancel()\n\n\t\tselect {\n\t\tcase err := <-errCh:\n\t\t\tif !errors.Is(err, bkErrors.ErrUserAborted) {\n\t\t\t\tt.Fatalf(\"expected user aborted error, got %T: %v\", err, err)\n\t\t\t}\n\t\t\tif strings.Contains(err.Error(), \"snapshot error\") {\n\t\t\t\tt.Fatalf(\"expected cancellation error without snapshot context, got: %v\", err)\n\t\t\t}\n\t\tcase <-time.After(2 * time.Second):\n\t\t\tt.Fatal(\"Run did not return promptly after cancellation\")\n\t\t}\n\n\t\tif got := apiRequests.Load(); got != 0 {\n\t\t\tt.Fatalf(\"expected no API requests after snapshot cancellation, got %d\", got)\n\t\t}\n\t})\n\n\tt.Run(\"returns user aborted and cleans up when canceled during build creation\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\tvar cancel context.CancelFunc\n\t\toriginalNotifyContext := notifyContext\n\t\tnotifyContext = func(parent context.Context, signals ...os.Signal) (context.Context, context.CancelFunc) {\n\t\t\tctx, stop := context.WithCancel(parent)\n\t\t\tcancel = stop\n\t\t\treturn ctx, stop\n\t\t}\n\t\tt.Cleanup(func() { notifyContext = originalNotifyContext })\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method == http.MethodPost && strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\t\tcancel()\n\t\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{Number: 1, State: \"scheduled\"})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\thttp.Error(w, \"unexpected request\", http.StatusInternalServerError)\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Interval: 2}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t}\n\t\tif !errors.Is(err, bkErrors.ErrUserAborted) {\n\t\t\tt.Fatalf(\"expected user aborted error, got %T: %v\", err, err)\n\t\t}\n\t\tif strings.Contains(err.Error(), \"API error\") || strings.Contains(err.Error(), \"creating preflight build\") {\n\t\t\tt.Fatalf(\"expected cancellation error without API build creation context, got: %v\", err)\n\t\t}\n\n\t\trefs := runGit(t, worktree, \"ls-remote\", \"--heads\", \"origin\")\n\t\tif strings.Contains(refs, \"bk/preflight/\") {\n\t\t\tt.Errorf(\"expected preflight branch to be cleaned up, but found: %s\", refs)\n\t\t}\n\t})\n\n\tt.Run(\"closes renderer when build creation fails\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\n\t\toriginalRendererFactory := rendererFactory\n\t\tfakeRenderer := &recordingRenderer{}\n\t\trendererFactory = func(io.Writer, bool, bool, context.CancelFunc) renderer {\n\t\t\treturn fakeRenderer\n\t\t}\n\t\tt.Cleanup(func() { rendererFactory = originalRendererFactory })\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tw.Write([]byte(`{\"message\":\"Authentication required\"}`))\n\t\t}))\n\t\tdefer s.Close()\n\t\tt.Setenv(\"BUILDKITE_REST_API_ENDPOINT\", s.URL)\n\n\t\tworktree := initTestRepo(t)\n\t\tt.Chdir(worktree)\n\n\t\tif err := os.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tcmd := &RunCmd{Pipeline: \"test-org/test-pipeline\", Interval: 2}\n\t\terr := cmd.Run(nil, stubGlobals{})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t}\n\t\tif !strings.Contains(err.Error(), \"creating preflight build\") {\n\t\t\tt.Fatalf(\"expected build creation error, got: %v\", err)\n\t\t}\n\t\tif fakeRenderer.closeCalls != 1 {\n\t\t\tt.Fatalf(\"expected renderer to be closed once, got %d\", fakeRenderer.closeCalls)\n\t\t}\n\t})\n}\n\ntype recordingRenderer struct {\n\tcloseCalls int\n}\n\nfunc (r *recordingRenderer) Render(Event) error { return nil }\n\nfunc (r *recordingRenderer) Close() error {\n\tr.closeCalls++\n\treturn nil\n}\n\nfunc initTestRepo(t *testing.T) string {\n\tt.Helper()\n\n\tdir := t.TempDir()\n\tworktree := filepath.Join(dir, \"work\")\n\tbare := filepath.Join(dir, \"origin.git\")\n\n\trunGit(t, \"\", \"init\", \"--bare\", bare)\n\trunGit(t, \"\", \"init\", worktree)\n\trunGit(t, worktree, \"config\", \"user.email\", \"test@test.com\")\n\trunGit(t, worktree, \"config\", \"user.name\", \"Test\")\n\trunGit(t, worktree, \"config\", \"commit.gpgsign\", \"false\")\n\n\tinitial := filepath.Join(worktree, \"README.md\")\n\tif err := os.WriteFile(initial, []byte(\"# test\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\trunGit(t, worktree, \"add\", \".\")\n\trunGit(t, worktree, \"commit\", \"-m\", \"initial commit\")\n\trunGit(t, worktree, \"remote\", \"add\", \"origin\", bare)\n\n\treturn worktree\n}\n\nfunc runGit(t *testing.T, dir string, args ...string) string {\n\tt.Helper()\n\n\tcmd := exec.Command(\"git\", args...)\n\tif dir != \"\" {\n\t\tcmd.Dir = dir\n\t}\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tt.Fatalf(\"git %s: %v\\n%s\", strings.Join(args, \" \"), err, out)\n\t}\n\treturn strings.TrimSpace(string(out))\n}\n\nfunc captureStdout(t *testing.T, fn func()) string {\n\tt.Helper()\n\n\toriginalStdout := os.Stdout\n\tr, w, err := os.Pipe()\n\tif err != nil {\n\t\tt.Fatalf(\"creating stdout pipe: %v\", err)\n\t}\n\n\tos.Stdout = w\n\tt.Cleanup(func() {\n\t\tos.Stdout = originalStdout\n\t})\n\n\tfn()\n\n\tif err := w.Close(); err != nil {\n\t\tt.Fatalf(\"closing stdout writer: %v\", err)\n\t}\n\n\tout, err := io.ReadAll(r)\n\tif err != nil {\n\t\tt.Fatalf(\"reading captured stdout: %v\", err)\n\t}\n\n\treturn string(out)\n}\n\nfunc decodeJSONLEvents(t *testing.T, output string) []Event {\n\tt.Helper()\n\n\tdecoder := json.NewDecoder(strings.NewReader(output))\n\tvar events []Event\n\tfor {\n\t\tvar event Event\n\t\tif err := decoder.Decode(&event); err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\treturn events\n\t\t\t}\n\t\t\tt.Fatalf(\"decode JSONL event: %v\\noutput:\\n%s\", err, output)\n\t\t}\n\t\tevents = append(events, event)\n\t}\n}\n"
  },
  {
    "path": "cmd/preflight/render.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tinternalpreflight \"github.com/buildkite/cli/v3/internal/preflight\"\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/mattn/go-runewidth\"\n)\n\ntype renderer interface {\n\tRender(Event) error\n\tClose() error\n}\n\nfunc newRenderer(stdout io.Writer, jsonMode bool, textMode bool, cancel context.CancelFunc) renderer {\n\tif jsonMode {\n\t\treturn newJSONRenderer(stdout)\n\t}\n\tisTTY := false\n\tif f, ok := stdout.(*os.File); ok {\n\t\tisTTY = isatty.IsTerminal(f.Fd())\n\t}\n\tif textMode || !isTTY {\n\t\treturn newPlainRenderer(stdout)\n\t}\n\treturn newTTYRenderer(cancel)\n}\n\ntype plainRenderer struct {\n\tstdout   io.Writer\n\tlastLine string\n}\n\nfunc newPlainRenderer(stdout io.Writer) *plainRenderer {\n\treturn &plainRenderer{stdout: stdout}\n}\n\nfunc (r *plainRenderer) Render(e Event) error {\n\tprefix := timestampPrefix(e.Time)\n\n\tswitch e.Type {\n\tcase EventOperation:\n\t\tif e.Detail != \"\" {\n\t\t\t_, err := fmt.Fprintf(r.stdout, \"%s\\n\", formatTimestampedDetail(e.Title, e.Detail, e.Time))\n\t\t\treturn err\n\t\t}\n\t\t_, err := fmt.Fprintf(r.stdout, \"%s%s\\n\", prefix, e.Title)\n\t\treturn err\n\n\tcase EventBuildStatus:\n\t\tline := fmt.Sprintf(\"Build #%d %s\", e.BuildNumber, e.BuildState)\n\t\tif e.Jobs != nil {\n\t\t\tif summary := e.Jobs.String(); summary != \"\" {\n\t\t\t\tline += \" — \" + summary\n\t\t\t}\n\t\t}\n\t\tif line != r.lastLine {\n\t\t\t_, err := fmt.Fprintf(r.stdout, \"%s%s\\n\", prefix, line)\n\t\t\tr.lastLine = line\n\t\t\treturn err\n\t\t}\n\n\tcase EventJobFailure:\n\t\tif e.Job != nil {\n\t\t\tpresenter := jobPresenter{pipeline: e.Pipeline, buildNumber: e.BuildNumber, buildURL: e.BuildURL}\n\t\t\t_, err := fmt.Fprintf(r.stdout, \"%s%s\\n\", prefix, presenter.Line(*e.Job))\n\t\t\treturn err\n\t\t}\n\n\tcase EventJobRetryPassed:\n\t\tif e.Job != nil {\n\t\t\tpresenter := jobPresenter{pipeline: e.Pipeline, buildNumber: e.BuildNumber, buildURL: e.BuildURL}\n\t\t\t_, err := fmt.Fprintf(r.stdout, \"%s%s\\n\", prefix, presenter.RetryPassedLine(*e.Job))\n\t\t\treturn err\n\t\t}\n\n\tcase EventBuildSummary:\n\t\theader := summaryHeader(e)\n\t\tif _, err := fmt.Fprintf(r.stdout, \"\\n%s\\n\", header); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif line := summaryBuildLine(e); line != \"\" {\n\t\t\tif _, err := fmt.Fprintf(r.stdout, \"  %s\\n\", line); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tpresenter := jobPresenter{pipeline: e.Pipeline, buildNumber: e.BuildNumber, buildURL: e.BuildURL}\n\t\tfor _, j := range e.PassedJobs {\n\t\t\tif _, err := fmt.Fprintf(r.stdout, \"  %s\\n\", presenter.PassedLine(j)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif _, err := fmt.Fprint(r.stdout, buildSummaryDetails(e, false, 0)); err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase EventTestFailure:\n\t\tif e.TestFailures != nil {\n\t\t\tpresenter := testPresenter{}\n\t\t\tfor _, t := range e.TestFailures {\n\t\t\t\tif _, err := fmt.Fprintf(r.stdout, \"%s\\n\", formatTimestampedBlock(presenter.Line(t), e.Time)); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (r *plainRenderer) Close() error { return nil }\n\ntype jsonRenderer struct {\n\tencoder *json.Encoder\n}\n\nfunc newJSONRenderer(stdout io.Writer) *jsonRenderer {\n\tenc := json.NewEncoder(stdout)\n\tenc.SetEscapeHTML(false)\n\treturn &jsonRenderer{encoder: enc}\n}\n\nfunc (r *jsonRenderer) Render(e Event) error {\n\treturn r.encoder.Encode(e)\n}\n\nfunc (r *jsonRenderer) Close() error { return nil }\n\nfunc summaryHeader(e Event) string {\n\tverdict := \"❌ Preflight Failed\"\n\tif e.Incomplete {\n\t\tverdict = \"❌ Preflight Incomplete\"\n\t\tif reason := summaryStopReasonLabel(e.StopReason); reason != \"\" {\n\t\t\tverdict = fmt.Sprintf(\"%s (%s)\", verdict, reason)\n\t\t}\n\t} else if e.BuildState == \"passed\" {\n\t\tverdict = \"✅ Preflight Passed\"\n\t}\n\tif e.Duration > 0 {\n\t\treturn fmt.Sprintf(\"%s (%s)\", verdict, formatDuration(e.Duration))\n\t}\n\treturn verdict\n}\n\nfunc summaryStopReasonLabel(reason string) string {\n\tif reason == \"\" {\n\t\treturn \"\"\n\t}\n\n\tswitch reason {\n\tcase \"build-failing\":\n\t\treturn \"build failing\"\n\tdefault:\n\t\treturn strings.ReplaceAll(reason, \"-\", \" \")\n\t}\n}\n\nfunc summaryBuildLine(e Event) string {\n\tlabel := summaryBuildLabel(e)\n\tif e.BuildURL == \"\" || label == \"\" {\n\t\treturn \"\"\n\t}\n\treturn fmt.Sprintf(\"%s: %s\", label, e.BuildURL)\n}\n\nfunc summaryBuildLabel(e Event) string {\n\tif e.BuildNumber > 0 {\n\t\treturn fmt.Sprintf(\"Build #%d\", e.BuildNumber)\n\t}\n\treturn \"\"\n}\n\nfunc formatDuration(d time.Duration) string {\n\td = d.Round(time.Second)\n\th := int(d.Hours())\n\tm := int(d.Minutes()) % 60\n\ts := int(d.Seconds()) % 60\n\tswitch {\n\tcase h > 0 && m > 0:\n\t\treturn fmt.Sprintf(\"%d %s %d %s\", h, plural(h, \"hour\"), m, plural(m, \"minute\"))\n\tcase h > 0:\n\t\treturn fmt.Sprintf(\"%d %s\", h, plural(h, \"hour\"))\n\tcase m > 0 && s > 0:\n\t\treturn fmt.Sprintf(\"%d %s %d %s\", m, plural(m, \"minute\"), s, plural(s, \"second\"))\n\tcase m > 0:\n\t\treturn fmt.Sprintf(\"%d %s\", m, plural(m, \"minute\"))\n\tdefault:\n\t\treturn fmt.Sprintf(\"%d %s\", s, plural(s, \"second\"))\n\t}\n}\n\nfunc plural(n int, word string) string {\n\tif n == 1 {\n\t\treturn word\n\t}\n\treturn word + \"s\"\n}\n\nconst summaryTestFailureDisplayLimit = 10\n\nfunc buildSummaryDetails(e Event, colored bool, width int) string {\n\tvar sections []string\n\n\tif len(e.FailedJobs) > 0 {\n\t\tpresenter := jobPresenter{pipeline: e.Pipeline, buildNumber: e.BuildNumber, buildURL: e.BuildURL}\n\t\tlines := []string{\"    Build Failures:\"}\n\t\tfor _, j := range e.FailedJobs {\n\t\t\tline := presenter.Line(j)\n\t\t\tif colored {\n\t\t\t\tline = presenter.ColoredLine(j)\n\t\t\t}\n\t\t\tlines = append(lines, \"        \"+line)\n\t\t}\n\t\tsections = append(sections, strings.Join(lines, \"\\n\"))\n\t}\n\n\tif testSection := summaryTestsSection(e.Tests.Runs, e.Tests.Failures, width); testSection != \"\" {\n\t\tsections = append(sections, testSection)\n\t}\n\n\tif len(sections) == 0 {\n\t\treturn \"\"\n\t}\n\n\treturn \"\\n\\n\" + strings.Join(sections, \"\\n\\n\") + \"\\n\"\n}\n\nfunc summaryTestsSection(tests map[string]internalpreflight.SummaryTestRun, failures []internalpreflight.SummaryTestFailure, width int) string {\n\tif len(tests) == 0 && len(failures) == 0 {\n\t\treturn \"\"\n\t}\n\n\tpresenter := testPresenter{}\n\tsummaries := orderedSummaryTestRuns(tests)\n\theader := \"    Tests Passed ✓\"\n\tfailedTests := 0\n\tfor _, summary := range summaries {\n\t\tfailedTests += summary.Failed\n\t}\n\ttotalFailed := max(failedTests, len(failures))\n\tif totalFailed > 0 {\n\t\theader = \"    Tests Failed ✗\"\n\t}\n\tlines := []string{header}\n\n\tif len(summaries) > 0 {\n\t\twidths := summarySuiteWidths(summaries)\n\t\tfor _, summary := range summaries {\n\t\t\tlines = append(lines, \"        \"+presenter.SummarySuiteLine(summary, widths))\n\t\t}\n\t}\n\n\tdisplayed := min(len(failures), summaryTestFailureDisplayLimit)\n\tif displayed > 0 {\n\t\tlines = append(lines, \"\")\n\t\tfor _, failure := range failures[:displayed] {\n\t\t\tlines = append(lines, presenter.SummaryFailureLine(failure, width, \"        \"))\n\t\t}\n\t}\n\n\tif remaining := totalFailed - displayed; remaining > 0 {\n\t\tlines = append(lines, fmt.Sprintf(\"        ... and %d more failed %s\", remaining, plural(remaining, \"test\")))\n\t}\n\n\treturn strings.Join(lines, \"\\n\")\n}\n\nfunc orderedSummaryTestRuns(tests map[string]internalpreflight.SummaryTestRun) []internalpreflight.SummaryTestRun {\n\tsummaries := make([]internalpreflight.SummaryTestRun, 0, len(tests))\n\tfor _, summary := range tests {\n\t\tsummaries = append(summaries, summary)\n\t}\n\n\tsort.SliceStable(summaries, func(i, j int) bool {\n\t\tleftFailed := summaries[i].Failed > 0\n\t\trightFailed := summaries[j].Failed > 0\n\t\tif leftFailed != rightFailed {\n\t\t\treturn leftFailed\n\t\t}\n\n\t\tleftLabel := strings.ToLower(summarySuiteLabel(summaries[i].SuiteName, summaries[i].SuiteSlug, \"unknown\"))\n\t\trightLabel := strings.ToLower(summarySuiteLabel(summaries[j].SuiteName, summaries[j].SuiteSlug, \"unknown\"))\n\t\treturn leftLabel < rightLabel\n\t})\n\n\treturn summaries\n}\n\nfunc summarySuiteWidths(tests []internalpreflight.SummaryTestRun) summarySuiteColumnWidths {\n\twidths := summarySuiteColumnWidths{Failed: 1, Passed: 1, Skipped: 1}\n\tfor _, summary := range tests {\n\t\twidths.Label = max(widths.Label, runewidth.StringWidth(summarySuiteLabel(summary.SuiteName, summary.SuiteSlug, \"unknown\")))\n\t\twidths.Failed = max(widths.Failed, len(strconv.Itoa(summary.Failed)))\n\t\twidths.Passed = max(widths.Passed, len(strconv.Itoa(summary.Passed)))\n\t\twidths.Skipped = max(widths.Skipped, len(strconv.Itoa(summary.Skipped)))\n\t}\n\n\treturn widths\n}\n\nfunc summarySuiteLabel(name, slug, fallback string) string {\n\tif name = strings.TrimSpace(name); name != \"\" {\n\t\treturn name\n\t}\n\n\tif slug = strings.TrimSpace(slug); slug != \"\" {\n\t\treturn slug\n\t}\n\n\treturn fallback\n}\n\nfunc jobLogCommand(pipeline string, buildNumber int, jobID string) string {\n\treturn fmt.Sprintf(\"bk job log -b %d -p %s %s\", buildNumber, pipeline, jobID)\n}\n\nfunc terminalHyperlink(label, url string) string {\n\tif url == \"\" {\n\t\treturn label\n\t}\n\treturn fmt.Sprintf(\"\\033]8;;%s\\033\\\\%s\\033]8;;\\033\\\\\", url, label)\n}\n\nfunc timestampPrefix(t time.Time) string {\n\treturn t.Format(time.TimeOnly) + \" \"\n}\n\nfunc formatTimestampedDetail(title, detail string, t time.Time) string {\n\treturn formatTimestampedBlock(title+\":\\n\"+detail, t)\n}\n\nfunc formatTimestampedBlock(text string, t time.Time) string {\n\tprefix := timestampPrefix(t)\n\thead, tail, ok := strings.Cut(text, \"\\n\")\n\tif !ok {\n\t\treturn prefix + head\n\t}\n\n\treturn prefix + head + \"\\n\" + indentAllLines(tail, len(prefix))\n}\n\nfunc indentAllLines(text string, indentWidth int) string {\n\tlines := strings.Split(text, \"\\n\")\n\tindent := strings.Repeat(\" \", indentWidth)\n\tfor i := range lines {\n\t\tlines[i] = indent + lines[i]\n\t}\n\treturn strings.Join(lines, \"\\n\")\n}\n"
  },
  {
    "path": "cmd/preflight/render_test.go",
    "content": "package preflight\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/buildkite/cli/v3/internal/build/watch\"\n\tinternalpreflight \"github.com/buildkite/cli/v3/internal/preflight\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nvar ansiCodesPattern = regexp.MustCompile(`\\x1b\\[[0-9;]*m`)\n\nfunc TestPlainRenderer_Render_Operation(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)\n\tr.Render(Event{\n\t\tType:  EventOperation,\n\t\tTime:  now,\n\t\tTitle: \"Creating snapshot of working tree...\",\n\t})\n\n\tgot := out.String()\n\tif !strings.Contains(got, \"10:30:00\") {\n\t\tt.Fatalf(\"expected timestamp, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"Creating snapshot of working tree...\") {\n\t\tt.Fatalf(\"expected title text, got %q\", got)\n\t}\n}\n\nfunc TestPlainRenderer_Render_OperationWithDetail(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)\n\tr.Render(Event{\n\t\tType:   EventOperation,\n\t\tTime:   now,\n\t\tTitle:  \"Creating snapshot of working tree...\",\n\t\tDetail: \"Commit: abc1234567\",\n\t})\n\n\tgot := out.String()\n\tindent := strings.Repeat(\" \", len(\"10:30:00 \"))\n\texpected := \"10:30:00 Creating snapshot of working tree...:\\n\" +\n\t\tindent + \"Commit: abc1234567\\n\"\n\tif got != expected {\n\t\tt.Fatalf(\"expected:\\n%s\\ngot:\\n%s\", expected, got)\n\t}\n}\n\nfunc TestPlainRenderer_Render_OperationWithMultiLineDetail(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)\n\tr.Render(Event{\n\t\tType:  EventOperation,\n\t\tTime:  now,\n\t\tTitle: \"Created snapshot of working tree...\",\n\t\tDetail: \"Commit: abc1234567\\nRef:    refs/heads/bk/preflight/abc123\\nFiles:  2 changed\\n\" +\n\t\t\t\"  ~ app/controllers/jobs_controller.rb\\n  ~ db/structure.sql\",\n\t})\n\n\tgot := out.String()\n\tindent := strings.Repeat(\" \", len(\"10:30:00 \"))\n\texpected := \"10:30:00 Created snapshot of working tree...:\\n\" +\n\t\tindent + \"Commit: abc1234567\\n\" +\n\t\tindent + \"Ref:    refs/heads/bk/preflight/abc123\\n\" +\n\t\tindent + \"Files:  2 changed\\n\" +\n\t\tindent + \"  ~ app/controllers/jobs_controller.rb\\n\" +\n\t\tindent + \"  ~ db/structure.sql\\n\"\n\tif got != expected {\n\t\tt.Fatalf(\"expected:\\n%s\\ngot:\\n%s\", expected, got)\n\t}\n}\n\nfunc TestFormatTimestampedDetail_UsesLeftAlignedTimestampIndent(t *testing.T) {\n\tnow := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)\n\n\tgot := formatTimestampedDetail(\"Created snapshot of working tree...\", \"Commit: abc1234567\\nRef: refs/heads/bk/preflight/abc123\", now)\n\n\tindent := strings.Repeat(\" \", len(\"10:30:00 \"))\n\texpected := \"10:30:00 Created snapshot of working tree...:\\n\" +\n\t\tindent + \"Commit: abc1234567\\n\" +\n\t\tindent + \"Ref: refs/heads/bk/preflight/abc123\"\n\tif got != expected {\n\t\tt.Fatalf(\"expected:\\n%s\\ngot:\\n%s\", expected, got)\n\t}\n}\n\nfunc TestFormatTimestampedBlock_IndentsContinuationLines(t *testing.T) {\n\tnow := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)\n\n\tgot := formatTimestampedBlock(\"  ❌ test: react/jsx-no-bind\\n    Location: .eslintrc.js:120\\n    Got 193 failures and 0 errors.\", now)\n\n\tindent := strings.Repeat(\" \", len(\"10:30:00 \"))\n\texpected := \"10:30:00   ❌ test: react/jsx-no-bind\\n\" +\n\t\tindent + \"    Location: .eslintrc.js:120\\n\" +\n\t\tindent + \"    Got 193 failures and 0 errors.\"\n\tif got != expected {\n\t\tt.Fatalf(\"expected:\\n%s\\ngot:\\n%s\", expected, got)\n\t}\n}\n\nfunc TestPlainRenderer_Render_BuildStatus(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 30, 5, 0, time.UTC)\n\tr.Render(Event{\n\t\tType:        EventBuildStatus,\n\t\tTime:        now,\n\t\tBuildNumber: 42,\n\t\tBuildState:  \"running\",\n\t\tJobs:        &watch.JobSummary{Passed: 8, Running: 3},\n\t})\n\n\tgot := out.String()\n\tif !strings.Contains(got, \"Build #42 running\") {\n\t\tt.Fatalf(\"expected build status line, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"8 passed\") {\n\t\tt.Fatalf(\"expected job summary, got %q\", got)\n\t}\n}\n\nfunc TestPlainRenderer_Render_BuildStatusDeduplicates(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 30, 5, 0, time.UTC)\n\te := Event{\n\t\tType:        EventBuildStatus,\n\t\tTime:        now,\n\t\tBuildNumber: 42,\n\t\tBuildState:  \"running\",\n\t\tJobs:        &watch.JobSummary{Running: 3},\n\t}\n\n\tr.Render(e)\n\tr.Render(e)\n\n\tlines := strings.Split(strings.TrimSpace(out.String()), \"\\n\")\n\tif len(lines) != 1 {\n\t\tt.Fatalf(\"expected 1 line (deduplicated), got %d: %v\", len(lines), lines)\n\t}\n}\n\nfunc TestPlainRenderer_Render_JobFailure(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 31, 0, 0, time.UTC)\n\texitOne := 1\n\tr.Render(Event{\n\t\tType: EventJobFailure,\n\t\tTime: now,\n\t\tJob: &buildkite.Job{\n\t\t\tID:         \"job-1\",\n\t\t\tName:       \"Lint\",\n\t\t\tType:       \"script\",\n\t\t\tState:      \"failed\",\n\t\t\tExitStatus: &exitOne,\n\t\t},\n\t})\n\n\tgot := out.String()\n\tif !strings.Contains(got, \"10:31:00\") {\n\t\tt.Fatalf(\"expected timestamp, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"Lint\") {\n\t\tt.Fatalf(\"expected job name, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"job-1\") {\n\t\tt.Fatalf(\"expected job ID, got %q\", got)\n\t}\n}\n\nfunc TestPlainRenderer_Render_JobRetryPassed(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 31, 0, 0, time.UTC)\n\tr.Render(Event{\n\t\tType: EventJobRetryPassed,\n\t\tTime: now,\n\t\tJob: &buildkite.Job{\n\t\t\tID:           \"retry-1\",\n\t\t\tName:         \"Lint\",\n\t\t\tType:         \"script\",\n\t\t\tState:        \"passed\",\n\t\t\tRetriesCount: 1,\n\t\t},\n\t})\n\n\tgot := out.String()\n\tif !strings.Contains(got, \"10:31:00\") {\n\t\tt.Fatalf(\"expected timestamp, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"✔ Lint\") {\n\t\tt.Fatalf(\"expected check mark and job name, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"passed on retry\") {\n\t\tt.Fatalf(\"expected retry text, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"attempt 2\") {\n\t\tt.Fatalf(\"expected attempt count, got %q\", got)\n\t}\n}\n\nfunc TestJSONRenderer_Render_JobRetryPassed(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newJSONRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 31, 0, 0, time.UTC)\n\tr.Render(Event{\n\t\tType:        EventJobRetryPassed,\n\t\tTime:        now,\n\t\tPreflightID: \"pfid-123\",\n\t\tPipeline:    \"buildkite/cli\",\n\t\tBuildNumber: 42,\n\t\tJob: &buildkite.Job{\n\t\t\tID:           \"retry-1\",\n\t\t\tName:         \"Lint\",\n\t\t\tState:        \"passed\",\n\t\t\tRetriesCount: 1,\n\t\t},\n\t})\n\n\tvar got Event\n\tif err := json.Unmarshal(out.Bytes(), &got); err != nil {\n\t\tt.Fatalf(\"invalid JSON: %v\", err)\n\t}\n\tif got.Type != EventJobRetryPassed {\n\t\tt.Fatalf(\"expected type %q, got %q\", EventJobRetryPassed, got.Type)\n\t}\n\tif got.Job == nil {\n\t\tt.Fatal(\"expected job to be present\")\n\t}\n\tif got.Job.RetriesCount != 1 {\n\t\tt.Fatalf(\"expected retries count 1, got %d\", got.Job.RetriesCount)\n\t}\n}\n\nfunc TestPlainRenderer_Render_TestFailure(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 31, 0, 0, time.UTC)\n\texecutionTime := buildkite.Timestamp{Time: now}\n\tr.Render(Event{\n\t\tType: EventTestFailure,\n\t\tTime: now,\n\t\tTestFailures: []buildkite.BuildTest{{\n\t\t\tName:            \"Test A\",\n\t\t\tExecutionsCount: 1,\n\t\t\tExecutionsCountByResult: buildkite.BuildTestExecutionsCount{\n\t\t\t\tFailed: 1,\n\t\t\t},\n\t\t\tExecutions: []buildkite.BuildTestExecution{{\n\t\t\t\tStatus:        \"failed\",\n\t\t\t\tLocation:      \"./spec/example_spec.rb:10\",\n\t\t\t\tFailureReason: \"Failure/Error: expect(false).to eq(true)\",\n\t\t\t\tTimestamp:     &executionTime,\n\t\t\t}},\n\t\t}},\n\t})\n\n\tgot := out.String()\n\tif ansiCodesPattern.MatchString(got) {\n\t\tt.Fatalf(\"expected plain output without ANSI codes, got %q\", got)\n\t}\n\n\tindent := strings.Repeat(\" \", len(\"10:31:00 \"))\n\texpected := \"10:31:00 ✗ Test A\\n\" +\n\t\tindent + \"    1 attempt (0 passed, 1 failed) — ./spec/example_spec.rb:10\\n\" +\n\t\tindent + \"    Failure/Error: expect(false).to eq(true)\\n\"\n\tif got != expected {\n\t\tt.Fatalf(\"expected:\\n%s\\ngot:\\n%s\", expected, got)\n\t}\n}\n\nfunc TestJSONRenderer_Render_Operation(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newJSONRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)\n\tr.Render(Event{\n\t\tType:        EventOperation,\n\t\tTime:        now,\n\t\tPreflightID: \"pfid-123\",\n\t\tTitle:       \"Creating snapshot of working tree...\",\n\t})\n\n\tvar got Event\n\tif err := json.Unmarshal(out.Bytes(), &got); err != nil {\n\t\tt.Fatalf(\"invalid JSON: %v\\n%s\", err, out.String())\n\t}\n\tif got.Type != EventOperation {\n\t\tt.Fatalf(\"expected type %q, got %q\", EventOperation, got.Type)\n\t}\n\tif got.Title != \"Creating snapshot of working tree...\" {\n\t\tt.Fatalf(\"expected title text, got %q\", got.Title)\n\t}\n\tif got.PreflightID != \"pfid-123\" {\n\t\tt.Fatalf(\"expected preflight ID, got %q\", got.PreflightID)\n\t}\n}\n\nfunc TestJSONRenderer_Render_BuildStatus(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newJSONRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 30, 5, 0, time.UTC)\n\tr.Render(Event{\n\t\tType:        EventBuildStatus,\n\t\tTime:        now,\n\t\tPreflightID: \"pfid-123\",\n\t\tPipeline:    \"buildkite/cli\",\n\t\tBuildNumber: 42,\n\t\tBuildURL:    \"https://buildkite.com/buildkite/cli/builds/42\",\n\t\tBuildState:  \"running\",\n\t\tJobs:        &watch.JobSummary{Passed: 8, Running: 3},\n\t})\n\n\tvar got Event\n\tif err := json.Unmarshal(out.Bytes(), &got); err != nil {\n\t\tt.Fatalf(\"invalid JSON: %v\", err)\n\t}\n\tif got.BuildNumber != 42 {\n\t\tt.Fatalf(\"expected build number 42, got %d\", got.BuildNumber)\n\t}\n\tif got.BuildState != \"running\" {\n\t\tt.Fatalf(\"expected build state running, got %q\", got.BuildState)\n\t}\n\tif got.Jobs.Passed != 8 {\n\t\tt.Fatalf(\"expected 8 passed, got %d\", got.Jobs.Passed)\n\t}\n}\n\nfunc TestJSONRenderer_Render_JobFailure(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newJSONRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 31, 0, 0, time.UTC)\n\texitOne := 1\n\tr.Render(Event{\n\t\tType:        EventJobFailure,\n\t\tTime:        now,\n\t\tPreflightID: \"pfid-123\",\n\t\tPipeline:    \"buildkite/cli\",\n\t\tBuildNumber: 42,\n\t\tJob: &buildkite.Job{\n\t\t\tID:         \"job-1\",\n\t\t\tName:       \"Lint\",\n\t\t\tState:      \"failed\",\n\t\t\tExitStatus: &exitOne,\n\t\t},\n\t})\n\n\tvar got Event\n\tif err := json.Unmarshal(out.Bytes(), &got); err != nil {\n\t\tt.Fatalf(\"invalid JSON: %v\", err)\n\t}\n\tif got.Type != EventJobFailure {\n\t\tt.Fatalf(\"expected type %q, got %q\", EventJobFailure, got.Type)\n\t}\n\tif got.Job == nil {\n\t\tt.Fatal(\"expected job to be present\")\n\t}\n\tif got.Job.ID != \"job-1\" {\n\t\tt.Fatalf(\"expected job ID job-1, got %q\", got.Job.ID)\n\t}\n\tif got.Job.Name != \"Lint\" {\n\t\tt.Fatalf(\"expected job name Lint, got %q\", got.Job.Name)\n\t}\n\tif got.Job.ExitStatus == nil || *got.Job.ExitStatus != 1 {\n\t\tt.Fatalf(\"expected exit status 1, got %v\", got.Job.ExitStatus)\n\t}\n}\n\nfunc TestJSONRenderer_Render_MultipleEvents_JSONL(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newJSONRenderer(&out)\n\n\tnow := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)\n\tr.Render(Event{Type: EventOperation, Time: now, Title: \"step 1\"})\n\tr.Render(Event{Type: EventOperation, Time: now, Title: \"step 2\"})\n\n\tlines := strings.Split(strings.TrimSpace(out.String()), \"\\n\")\n\tif len(lines) != 2 {\n\t\tt.Fatalf(\"expected 2 JSONL lines, got %d\", len(lines))\n\t}\n\tfor i, line := range lines {\n\t\tvar e Event\n\t\tif err := json.Unmarshal([]byte(line), &e); err != nil {\n\t\t\tt.Fatalf(\"line %d: invalid JSON: %v\", i, err)\n\t\t}\n\t}\n}\n\nfunc TestTestPresenter_Line_FailedAttemptIncludesSummaryAndFailureDetails(t *testing.T) {\n\tt.Parallel()\n\tolder := buildkite.Timestamp{Time: time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)}\n\tnewer := buildkite.Timestamp{Time: time.Date(2025, 1, 15, 10, 31, 0, 0, time.UTC)}\n\n\tline := testPresenter{}.Line(buildkite.BuildTest{\n\t\tName:            \"Pipelines::ShardMigration::DeleteOrganizationFromShardWorker with more than BATCH_SIZE records for a shard that needs cleaning\",\n\t\tLocation:        \"./spec/workers/pipelines/shard_migration/delete_organization_from_shard_worker_spec.rb:181\",\n\t\tExecutionsCount: 2,\n\t\tExecutionsCountByResult: buildkite.BuildTestExecutionsCount{\n\t\t\tFailed: 2,\n\t\t},\n\t\tExecutions: []buildkite.BuildTestExecution{\n\t\t\t{\n\t\t\t\tStatus:        \"failed\",\n\t\t\t\tLocation:      \"./spec/workers/pipelines/shard_migration/delete_organization_from_shard_worker_spec.rb:181\",\n\t\t\t\tFailureReason: \"Failure/Error: expect(empty_tables).to eq({})\",\n\t\t\t\tTimestamp:     &newer,\n\t\t\t},\n\t\t\t{\n\t\t\t\tStatus:        \"failed\",\n\t\t\t\tLocation:      \"./spec/workers/pipelines/shard_migration/delete_organization_from_shard_worker_spec.rb:182\",\n\t\t\t\tFailureReason: \"Failure/Error: expect(empty_tables).to eq({})\",\n\t\t\t\tTimestamp:     &older,\n\t\t\t},\n\t\t},\n\t})\n\n\tif ansiCodesPattern.MatchString(line) {\n\t\tt.Fatalf(\"expected plain line without ANSI codes, got %q\", line)\n\t}\n\n\tgot := line\n\n\tif !strings.Contains(got, \"✗ Pipelines::ShardMigration::DeleteOrgan...records for a shard that needs cleaning\") {\n\t\tt.Fatalf(\"expected long name to preserve the start and end, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"2 attempts (0 passed, 2 failed) — ./spec/workers/pipelines/shard_migration/delete_organization_from_shard_worker_spec.rb:181\") {\n\t\tt.Fatalf(\"expected location detail, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"Failure/Error: expect(empty_tables).to eq({})\") {\n\t\tt.Fatalf(\"expected failure reason, got %q\", got)\n\t}\n\tif strings.Contains(got, \"BATCH_SIZE records for a shard that needs cleaning\") {\n\t\tt.Fatalf(\"expected long name to be truncated, got %q\", got)\n\t}\n}\n\nfunc TestFormatTestStatusIcon_UsesLatestExecution(t *testing.T) {\n\tt.Parallel()\n\tnewest := buildkite.Timestamp{Time: time.Date(2025, 1, 15, 10, 31, 0, 0, time.UTC)}\n\n\texecution := &buildkite.BuildTestExecution{Status: \"passed\", Timestamp: &newest}\n\ticon := formatTestStatusIcon(execution, false)\n\n\tif got, want := icon, \"✓\"; got != want {\n\t\tt.Fatalf(\"icon = %q, want %q\", got, want)\n\t}\n}\n\nfunc TestFormatTestStatusIcon_NilExecution(t *testing.T) {\n\tt.Parallel()\n\n\ticon := formatTestStatusIcon(nil, false)\n\n\tif got, want := icon, \"?\"; got != want {\n\t\tt.Fatalf(\"icon = %q, want %q\", got, want)\n\t}\n}\n\nfunc TestTestAttemptCounts_FormatsCorrectly(t *testing.T) {\n\tt.Parallel()\n\n\tcounts := testAttemptCounts(buildkite.BuildTest{\n\t\tExecutionsCount: 5,\n\t\tExecutionsCountByResult: buildkite.BuildTestExecutionsCount{\n\t\t\tPassed: 3,\n\t\t\tFailed: 2,\n\t\t},\n\t\tExecutions: []buildkite.BuildTestExecution{{Status: \"failed\"}},\n\t})\n\n\tif got, want := counts, \"5 attempts (3 passed, 2 failed)\"; got != want {\n\t\tt.Fatalf(\"counts = %q, want %q\", got, want)\n\t}\n}\n\nfunc TestTestPresenter_Line_PassedLatestAttemptOnlyShowsSummaryLine(t *testing.T) {\n\tt.Parallel()\n\toldest := buildkite.Timestamp{Time: time.Date(2025, 1, 15, 10, 29, 0, 0, time.UTC)}\n\tmiddle := buildkite.Timestamp{Time: time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)}\n\tnewest := buildkite.Timestamp{Time: time.Date(2025, 1, 15, 10, 31, 0, 0, time.UTC)}\n\n\tline := testPresenter{}.Line(buildkite.BuildTest{\n\t\tName:            \"Test A\",\n\t\tLocation:        \"./spec/example_spec.rb:10\",\n\t\tExecutionsCount: 3,\n\t\tExecutionsCountByResult: buildkite.BuildTestExecutionsCount{\n\t\t\tPassed: 1,\n\t\t\tFailed: 2,\n\t\t},\n\t\tExecutions: []buildkite.BuildTestExecution{\n\t\t\t{Status: \"passed\", Location: \"./spec/example_spec.rb:10\", Timestamp: &newest},\n\t\t\t{Status: \"failed\", FailureReason: \"Failure/Error: expect(false).to eq(true)\", Location: \"./spec/example_spec.rb:10\", Timestamp: &oldest},\n\t\t\t{Status: \"failed\", FailureReason: \"Failure/Error: expect(false).to eq(true)\", Location: \"./spec/example_spec.rb:10\", Timestamp: &middle},\n\t\t},\n\t})\n\n\tif ansiCodesPattern.MatchString(line) {\n\t\tt.Fatalf(\"expected plain line without ANSI codes, got %q\", line)\n\t}\n\n\tgot := line\n\n\tif strings.Contains(got, \"./spec/example_spec.rb:10\") {\n\t\tt.Fatalf(\"expected passed attempt to omit location detail, got %q\", got)\n\t}\n\tif strings.Contains(got, \"Failure/Error:\") {\n\t\tt.Fatalf(\"expected passed attempt to omit failure detail, got %q\", got)\n\t}\n}\n\nfunc TestLatestTestExecution_PicksNewestTimestamp(t *testing.T) {\n\tt.Parallel()\n\tolder := buildkite.Timestamp{Time: time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)}\n\tnewer := buildkite.Timestamp{Time: time.Date(2025, 1, 15, 10, 31, 0, 0, time.UTC)}\n\n\texecution := latestTestExecution(buildkite.BuildTest{\n\t\tExecutions: []buildkite.BuildTestExecution{\n\t\t\t{Status: \"failed\", Location: \"./spec/example_spec.rb:11\", Timestamp: &older},\n\t\t\t{Status: \"passed\", Location: \"./spec/example_spec.rb:12\", Timestamp: &newer},\n\t\t\t{Status: \"failed\", Location: \"./spec/example_spec.rb:10\"},\n\t\t},\n\t})\n\n\tif execution == nil {\n\t\tt.Fatal(\"expected execution to be present\")\n\t\treturn\n\t}\n\tif got, want := execution.Status, \"passed\"; got != want {\n\t\tt.Fatalf(\"status = %q, want %q\", got, want)\n\t}\n\tif got, want := execution.Location, \"./spec/example_spec.rb:12\"; got != want {\n\t\tt.Fatalf(\"location = %q, want %q\", got, want)\n\t}\n}\n\nfunc TestLatestTestExecution_IgnoresExecutionsWithoutTimestamps(t *testing.T) {\n\tt.Parallel()\n\n\texecution := latestTestExecution(buildkite.BuildTest{\n\t\tExecutions: []buildkite.BuildTestExecution{\n\t\t\t{Status: \"failed\", Location: \"./spec/example_spec.rb:10\"},\n\t\t\t{Status: \"passed\", Location: \"./spec/example_spec.rb:11\"},\n\t\t},\n\t})\n\n\tif execution != nil {\n\t\tt.Fatalf(\"expected nil execution, got %#v\", execution)\n\t}\n}\n\nfunc TestTestPresenter_ColoredLine_AddsANSIStyles(t *testing.T) {\n\tt.Parallel()\n\texecutionTime := buildkite.Timestamp{Time: time.Date(2025, 1, 15, 10, 31, 0, 0, time.UTC)}\n\n\tline := testPresenter{}.ColoredLine(buildkite.BuildTest{\n\t\tName:            \"Test A\",\n\t\tExecutionsCount: 1,\n\t\tExecutionsCountByResult: buildkite.BuildTestExecutionsCount{\n\t\t\tFailed: 1,\n\t\t},\n\t\tExecutions: []buildkite.BuildTestExecution{{\n\t\t\tStatus:        \"failed\",\n\t\t\tLocation:      \"./spec/example_spec.rb:10\",\n\t\t\tFailureReason: \"Failure/Error: expect(false).to eq(true)\",\n\t\t\tTimestamp:     &executionTime,\n\t\t}},\n\t})\n\n\tif !ansiCodesPattern.MatchString(line) {\n\t\tt.Fatalf(\"expected colored line with ANSI codes, got %q\", line)\n\t}\n\n\tgot := stripANSI(line)\n\tif !strings.Contains(got, \"✗ Test A\") {\n\t\tt.Fatalf(\"expected colored line to preserve headline text content, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"1 attempt (0 passed, 1 failed) — ./spec/example_spec.rb:10\") {\n\t\tt.Fatalf(\"expected colored line to preserve text content, got %q\", got)\n\t}\n}\n\nfunc stripANSI(s string) string {\n\treturn ansiCodesPattern.ReplaceAllString(s, \"\")\n}\n\nfunc TestNewRenderer_NonFileWriterDefaultsToPlain(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newRenderer(&out, false, false, func() {})\n\tif _, ok := r.(*plainRenderer); !ok {\n\t\tt.Fatalf(\"expected *plainRenderer when stdout is a non-file io.Writer, got %T\", r)\n\t}\n}\n\nfunc TestNewRenderer_TextModeForcesPlain(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newRenderer(&out, false, true, func() {})\n\tif _, ok := r.(*plainRenderer); !ok {\n\t\tt.Fatalf(\"expected *plainRenderer when --text is set, got %T\", r)\n\t}\n}\n\nfunc TestNewRenderer_JSONModeReturnsJSON(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newRenderer(&out, true, false, func() {})\n\tif _, ok := r.(*jsonRenderer); !ok {\n\t\tt.Fatalf(\"expected *jsonRenderer when --json is set, got %T\", r)\n\t}\n}\n\nfunc TestPlainRenderer_Render_BuildSummaryPassed(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\tif err := r.Render(Event{\n\t\tType:        EventBuildSummary,\n\t\tTime:        time.Date(2025, 1, 15, 10, 32, 0, 0, time.UTC),\n\t\tPipeline:    \"buildkite/cli\",\n\t\tBuildNumber: 42,\n\t\tBuildURL:    \"https://buildkite.com/buildkite/cli/builds/42\",\n\t\tBuildState:  \"passed\",\n\t\tPassedJobs: []buildkite.Job{\n\t\t\t{ID: \"job-1\", Name: \"Lint\", Type: \"script\", State: \"passed\"},\n\t\t\t{ID: \"job-2\", Name: \"Test\", Type: \"script\", State: \"passed\"},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"Render() error: %v\", err)\n\t}\n\n\tgot := out.String()\n\tif !strings.Contains(got, \"✅ Preflight Passed\") {\n\t\tt.Fatalf(\"expected passed header, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"Build #42: https://buildkite.com/buildkite/cli/builds/42\") {\n\t\tt.Fatalf(\"expected build URL in summary, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"Lint\") {\n\t\tt.Fatalf(\"expected passed job name, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"Test\") {\n\t\tt.Fatalf(\"expected passed job name, got %q\", got)\n\t}\n}\n\nfunc TestPlainRenderer_Render_BuildSummaryFailed(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\texitOne := 1\n\tif err := r.Render(Event{\n\t\tType:        EventBuildSummary,\n\t\tTime:        time.Date(2025, 1, 15, 10, 32, 0, 0, time.UTC),\n\t\tPipeline:    \"buildkite/cli\",\n\t\tBuildNumber: 42,\n\t\tBuildURL:    \"https://buildkite.com/buildkite/cli/builds/42\",\n\t\tBuildState:  \"failed\",\n\t\tFailedJobs: []buildkite.Job{\n\t\t\t{ID: \"job-1\", Name: \"Lint\", Type: \"script\", State: \"failed\", ExitStatus: &exitOne},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"Render() error: %v\", err)\n\t}\n\n\tgot := out.String()\n\tif !strings.Contains(got, \"❌ Preflight Failed\") {\n\t\tt.Fatalf(\"expected failed header, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"Build #42: https://buildkite.com/buildkite/cli/builds/42\") {\n\t\tt.Fatalf(\"expected build URL in summary, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"Build Failures:\") {\n\t\tt.Fatalf(\"expected build failures section, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"Lint\") {\n\t\tt.Fatalf(\"expected hard-failed job name, got %q\", got)\n\t}\n\tif strings.Contains(got, \"Optional check\") {\n\t\tt.Fatalf(\"soft-failed job should not appear in summary, got %q\", got)\n\t}\n}\n\nfunc TestPlainRenderer_Render_BuildSummaryStoppedEarly(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\tbuildCanceled := false\n\texitOne := 1\n\tif err := r.Render(Event{\n\t\tType:          EventBuildSummary,\n\t\tTime:          time.Date(2025, 1, 15, 10, 32, 0, 0, time.UTC),\n\t\tPipeline:      \"buildkite/cli\",\n\t\tBuildNumber:   42,\n\t\tBuildURL:      \"https://buildkite.com/buildkite/cli/builds/42\",\n\t\tBuildState:    \"failing\",\n\t\tIncomplete:    true,\n\t\tStopReason:    \"build-failing\",\n\t\tBuildCanceled: &buildCanceled,\n\t\tFailedJobs: []buildkite.Job{\n\t\t\t{ID: \"job-1\", Name: \"Lint\", Type: \"script\", State: \"failed\", ExitStatus: &exitOne},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"Render() error: %v\", err)\n\t}\n\n\tgot := out.String()\n\tif !strings.Contains(got, \"❌ Preflight Incomplete (build failing)\") {\n\t\tt.Fatalf(\"expected early-stop header, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"Build Failures:\") {\n\t\tt.Fatalf(\"expected build failures section, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"Lint\") {\n\t\tt.Fatalf(\"expected failed job name, got %q\", got)\n\t}\n}\n\nfunc TestPlainRenderer_Render_BuildSummaryIncludesTests(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newPlainRenderer(&out)\n\n\tif err := r.Render(Event{\n\t\tType:       EventBuildSummary,\n\t\tTime:       time.Date(2025, 1, 15, 10, 32, 0, 0, time.UTC),\n\t\tBuildState: \"failed\",\n\t\tTests: internalpreflight.SummaryTests{\n\t\t\tRuns: map[string]internalpreflight.SummaryTestRun{\n\t\t\t\t\"run-go\":    {RunID: \"run-go\", SuiteName: \"Go\", SuiteSlug: \"go\", Passed: 12, Failed: 1, Skipped: 0},\n\t\t\t\t\"run-rspec\": {RunID: \"run-rspec\", SuiteName: \"RSpec\", SuiteSlug: \"rspec\", Passed: 47, Failed: 2, Skipped: 3},\n\t\t\t},\n\t\t\tFailures: []internalpreflight.SummaryTestFailure{{\n\t\t\t\tRunID:     \"run-rspec\",\n\t\t\t\tSuiteName: \"RSpec\",\n\t\t\t\tSuiteSlug: \"rspec\",\n\t\t\t\tName:      \"AuthService.validateToken handles expired tokens\",\n\t\t\t\tLocation:  \"src/auth.test.ts:89\",\n\t\t\t\tMessage:   \"Expected 'expired' but got 'invalid'\",\n\t\t\t}},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"Render() error: %v\", err)\n\t}\n\n\tgot := out.String()\n\tif !strings.Contains(got, \"Tests Failed ✗\") {\n\t\tt.Fatalf(\"expected tests section, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"✗ Go     1 failed  12 passed  0 skipped\") {\n\t\tt.Fatalf(\"expected go test summary, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"✗ RSpec  2 failed  47 passed  3 skipped\") {\n\t\tt.Fatalf(\"expected rspec test summary, got %q\", got)\n\t}\n\tif !strings.Contains(got, \"✗ [RSpec] src/auth.test.ts:89 — AuthService.validateToken handles expired tokens\") {\n\t\tt.Fatalf(\"expected failure header from endpoint summary, got %q\", got)\n\t}\n\tif strings.Contains(got, \"Expected 'expired' but got 'invalid'\") {\n\t\tt.Fatalf(\"expected summary failure line to omit failure message, got %q\", got)\n\t}\n}\n\nfunc TestJSONRenderer_Render_BuildSummaryPassed(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newJSONRenderer(&out)\n\n\tif err := r.Render(Event{\n\t\tType:        EventBuildSummary,\n\t\tTime:        time.Date(2025, 1, 15, 10, 32, 0, 0, time.UTC),\n\t\tPreflightID: \"pfid-123\",\n\t\tPipeline:    \"buildkite/cli\",\n\t\tBuildNumber: 42,\n\t\tBuildState:  \"passed\",\n\t}); err != nil {\n\t\tt.Fatalf(\"Render() error: %v\", err)\n\t}\n\n\tvar got map[string]any\n\tif err := json.Unmarshal(out.Bytes(), &got); err != nil {\n\t\tt.Fatalf(\"invalid JSON: %v\\n%s\", err, out.String())\n\t}\n\tif got[\"type\"] != \"build_summary\" {\n\t\tt.Fatalf(\"expected type build_summary, got %v\", got[\"type\"])\n\t}\n\tif got[\"build_state\"] != \"passed\" {\n\t\tt.Fatalf(\"expected build_state=passed, got %v\", got[\"build_state\"])\n\t}\n\tif got[\"failed_jobs\"] != nil {\n\t\tt.Fatalf(\"expected no failed_jobs for passing build, got %v\", got[\"failed_jobs\"])\n\t}\n}\n\nfunc TestJSONRenderer_Render_BuildSummaryFailed(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newJSONRenderer(&out)\n\n\texitOne := 1\n\tif err := r.Render(Event{\n\t\tType:        EventBuildSummary,\n\t\tTime:        time.Date(2025, 1, 15, 10, 32, 0, 0, time.UTC),\n\t\tPreflightID: \"pfid-123\",\n\t\tPipeline:    \"buildkite/cli\",\n\t\tBuildNumber: 42,\n\t\tBuildState:  \"failed\",\n\t\tFailedJobs: []buildkite.Job{\n\t\t\t{ID: \"job-1\", Name: \"Lint\", Type: \"script\", State: \"failed\", ExitStatus: &exitOne},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"Render() error: %v\", err)\n\t}\n\n\tvar got map[string]any\n\tif err := json.Unmarshal(out.Bytes(), &got); err != nil {\n\t\tt.Fatalf(\"invalid JSON: %v\\n%s\", err, out.String())\n\t}\n\tif got[\"build_state\"] != \"failed\" {\n\t\tt.Fatalf(\"expected build_state=failed, got %v\", got[\"build_state\"])\n\t}\n\tfailedJobs, ok := got[\"failed_jobs\"].([]any)\n\tif !ok || len(failedJobs) != 1 {\n\t\tt.Fatalf(\"expected 1 failed job, got %v\", got[\"failed_jobs\"])\n\t}\n}\n\nfunc TestJSONRenderer_Render_BuildSummaryStoppedEarly(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newJSONRenderer(&out)\n\n\tbuildCanceled := false\n\tif err := r.Render(Event{\n\t\tType:          EventBuildSummary,\n\t\tTime:          time.Date(2025, 1, 15, 10, 32, 0, 0, time.UTC),\n\t\tPreflightID:   \"pfid-123\",\n\t\tPipeline:      \"buildkite/cli\",\n\t\tBuildNumber:   42,\n\t\tBuildState:    \"failing\",\n\t\tIncomplete:    true,\n\t\tStopReason:    \"build-failing\",\n\t\tBuildCanceled: &buildCanceled,\n\t}); err != nil {\n\t\tt.Fatalf(\"Render() error: %v\", err)\n\t}\n\n\tvar got map[string]any\n\tif err := json.Unmarshal(out.Bytes(), &got); err != nil {\n\t\tt.Fatalf(\"invalid JSON: %v\\n%s\", err, out.String())\n\t}\n\tif got[\"type\"] != \"build_summary\" {\n\t\tt.Fatalf(\"expected type build_summary, got %v\", got[\"type\"])\n\t}\n\tif got[\"incomplete\"] != true {\n\t\tt.Fatalf(\"expected incomplete=true, got %v\", got[\"incomplete\"])\n\t}\n\tif got[\"stop_reason\"] != \"build-failing\" {\n\t\tt.Fatalf(\"expected stop_reason=build-failing, got %v\", got[\"stop_reason\"])\n\t}\n\tif got[\"build_canceled\"] != false {\n\t\tt.Fatalf(\"expected build_canceled=false, got %v\", got[\"build_canceled\"])\n\t}\n}\n\nfunc TestJSONRenderer_Render_BuildSummaryIncludesTests(t *testing.T) {\n\tvar out bytes.Buffer\n\tr := newJSONRenderer(&out)\n\n\tif err := r.Render(Event{\n\t\tType:        EventBuildSummary,\n\t\tTime:        time.Date(2025, 1, 15, 10, 32, 0, 0, time.UTC),\n\t\tPreflightID: \"pfid-123\",\n\t\tBuildState:  \"failed\",\n\t\tTests: internalpreflight.SummaryTests{\n\t\t\tRuns: map[string]internalpreflight.SummaryTestRun{\n\t\t\t\t\"run-rspec\": {RunID: \"run-rspec\", SuiteName: \"RSpec\", SuiteSlug: \"rspec\", Passed: 47, Failed: 2, Skipped: 3},\n\t\t\t},\n\t\t\tFailures: []internalpreflight.SummaryTestFailure{{\n\t\t\t\tRunID:     \"run-rspec\",\n\t\t\t\tSuiteName: \"RSpec\",\n\t\t\t\tSuiteSlug: \"rspec\",\n\t\t\t\tName:      \"AuthService.validateToken handles expired tokens\",\n\t\t\t\tLocation:  \"src/auth.test.ts:89\",\n\t\t\t\tMessage:   \"Expected 'expired' but got 'invalid'\",\n\t\t\t}},\n\t\t},\n\t}); err != nil {\n\t\tt.Fatalf(\"Render() error: %v\", err)\n\t}\n\n\tvar got map[string]any\n\tif err := json.Unmarshal(out.Bytes(), &got); err != nil {\n\t\tt.Fatalf(\"invalid JSON: %v\\n%s\", err, out.String())\n\t}\n\ttests, ok := got[\"tests\"].(map[string]any)\n\tif !ok {\n\t\tt.Fatalf(\"expected tests object, got %v\", got[\"tests\"])\n\t}\n\truns, ok := tests[\"runs\"].(map[string]any)\n\tif !ok {\n\t\tt.Fatalf(\"expected tests.runs object, got %v\", tests[\"runs\"])\n\t}\n\trspec, ok := runs[\"run-rspec\"].(map[string]any)\n\tif !ok {\n\t\tt.Fatalf(\"expected rspec summary, got %v\", runs[\"run-rspec\"])\n\t}\n\tif rspec[\"passed\"] != float64(47) || rspec[\"failed\"] != float64(2) || rspec[\"skipped\"] != float64(3) {\n\t\tt.Fatalf(\"unexpected rspec summary: %v\", rspec)\n\t}\n\tif rspec[\"suite_name\"] != \"RSpec\" || rspec[\"suite_slug\"] != \"rspec\" || rspec[\"run_id\"] != \"run-rspec\" {\n\t\tt.Fatalf(\"unexpected rspec identifiers: %v\", rspec)\n\t}\n\tfailures, ok := tests[\"failures\"].([]any)\n\tif !ok || len(failures) != 1 {\n\t\tt.Fatalf(\"expected one failure, got %v\", tests[\"failures\"])\n\t}\n\tfailure, ok := failures[0].(map[string]any)\n\tif !ok {\n\t\tt.Fatalf(\"expected failure object, got %v\", failures[0])\n\t}\n\tif failure[\"suite_name\"] != \"RSpec\" || failure[\"suite_slug\"] != \"rspec\" || failure[\"run_id\"] != \"run-rspec\" {\n\t\tt.Fatalf(\"unexpected failure identifiers: %v\", failure)\n\t}\n}\n\nfunc TestFormatDuration(t *testing.T) {\n\ttests := []struct {\n\t\td    time.Duration\n\t\twant string\n\t}{\n\t\t{1 * time.Second, \"1 second\"},\n\t\t{30 * time.Second, \"30 seconds\"},\n\t\t{1 * time.Minute, \"1 minute\"},\n\t\t{1*time.Minute + 1*time.Second, \"1 minute 1 second\"},\n\t\t{1*time.Minute + 30*time.Second, \"1 minute 30 seconds\"},\n\t\t{90 * time.Second, \"1 minute 30 seconds\"},\n\t\t{10 * time.Minute, \"10 minutes\"},\n\t\t{10*time.Minute + 23*time.Second, \"10 minutes 23 seconds\"},\n\t\t{1 * time.Hour, \"1 hour\"},\n\t\t{1*time.Hour + 1*time.Minute, \"1 hour 1 minute\"},\n\t\t{2 * time.Hour, \"2 hours\"},\n\t\t{2*time.Hour + 5*time.Minute, \"2 hours 5 minutes\"},\n\t}\n\tfor _, tt := range tests {\n\t\tif got := formatDuration(tt.d); got != tt.want {\n\t\t\tt.Errorf(\"formatDuration(%v) = %q, want %q\", tt.d, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc scriptJob(id, name, state string, softFailed bool, startedAt, finishedAt *buildkite.Timestamp, exitStatus *int) buildkite.Job {\n\treturn buildkite.Job{\n\t\tID:         id,\n\t\tName:       name,\n\t\tType:       \"script\",\n\t\tState:      state,\n\t\tSoftFailed: softFailed,\n\t\tStartedAt:  startedAt,\n\t\tFinishedAt: finishedAt,\n\t\tExitStatus: exitStatus,\n\t}\n}\n"
  },
  {
    "path": "cmd/preflight/result.go",
    "content": "package preflight\n\nimport (\n\t\"fmt\"\n\n\tbuildstate \"github.com/buildkite/cli/v3/internal/build/state\"\n\tbkErrors \"github.com/buildkite/cli/v3/internal/errors\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype resultKind int\n\nconst (\n\tresultCompletedPass resultKind = iota\n\tresultCompletedFailure\n\tresultIncompleteFailure\n\tresultIncomplete\n\tresultUnknown\n)\n\ntype Result struct {\n\tkind       resultKind\n\tbuildState string\n}\n\nfunc NewResult(build buildkite.Build) Result {\n\tstate := buildstate.State(build.State)\n\n\tif state == buildstate.Passed {\n\t\treturn Result{kind: resultCompletedPass, buildState: build.State}\n\t}\n\n\tif buildstate.IsTerminal(state) {\n\t\treturn Result{kind: resultCompletedFailure, buildState: build.State}\n\t}\n\n\tif state == buildstate.Failing {\n\t\treturn Result{kind: resultIncompleteFailure, buildState: build.State}\n\t}\n\n\tif buildstate.IsIncomplete(state) {\n\t\treturn Result{kind: resultIncomplete, buildState: build.State}\n\t}\n\n\treturn Result{kind: resultUnknown, buildState: build.State}\n}\n\n// Passed reports whether the build completed successfully.\nfunc (r Result) Passed() bool {\n\treturn r.kind == resultCompletedPass\n}\n\nfunc (r Result) Error() error {\n\tswitch r.kind {\n\tcase resultCompletedPass:\n\t\treturn nil\n\tcase resultCompletedFailure:\n\t\treturn bkErrors.NewPreflightCompletedFailureError(nil, fmt.Sprintf(\"build is %s\", r.buildState))\n\tcase resultIncompleteFailure:\n\t\treturn bkErrors.NewPreflightIncompleteFailureError(nil, fmt.Sprintf(\"build is %s\", r.buildState))\n\tcase resultIncomplete:\n\t\treturn bkErrors.NewPreflightIncompleteError(nil, fmt.Sprintf(\"build is %s\", r.buildState))\n\tcase resultUnknown:\n\t\treturn bkErrors.NewPreflightUnknownError(\n\t\t\tnil,\n\t\t\tfmt.Sprintf(\"build is %s\", r.buildState),\n\t\t)\n\tdefault:\n\t\treturn bkErrors.NewInternalError(\n\t\t\tnil,\n\t\t\tfmt.Sprintf(\"unexpected result kind %d for build state '%s', unable to coerce to error\", r.kind, r.buildState),\n\t\t\t\"This is likely a bug\",\n\t\t\t\"Report to Buildkite\",\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "cmd/preflight/result_test.go",
    "content": "package preflight\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tbkErrors \"github.com/buildkite/cli/v3/internal/errors\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestResult(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tbuild buildkite.Build\n\t\twant  Result\n\t}{\n\t\t{\n\t\t\tname:  \"clean pass\",\n\t\t\tbuild: buildkite.Build{State: \"passed\"},\n\t\t\twant:  Result{kind: resultCompletedPass, buildState: \"passed\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"terminal failed build is completed failure\",\n\t\t\tbuild: buildkite.Build{State: \"failed\"},\n\t\t\twant:  Result{kind: resultCompletedFailure, buildState: \"failed\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"failing build is incomplete failure\",\n\t\t\tbuild: buildkite.Build{State: \"failing\"},\n\t\t\twant:  Result{kind: resultIncompleteFailure, buildState: \"failing\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"running build is incomplete\",\n\t\t\tbuild: buildkite.Build{State: \"running\"},\n\t\t\twant:  Result{kind: resultIncomplete, buildState: \"running\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"scheduled build is incomplete\",\n\t\t\tbuild: buildkite.Build{State: \"scheduled\"},\n\t\t\twant:  Result{kind: resultIncomplete, buildState: \"scheduled\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"blocked build is incomplete\",\n\t\t\tbuild: buildkite.Build{State: \"blocked\"},\n\t\t\twant:  Result{kind: resultIncomplete, buildState: \"blocked\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"canceling build is incomplete\",\n\t\t\tbuild: buildkite.Build{State: \"canceling\"},\n\t\t\twant:  Result{kind: resultIncomplete, buildState: \"canceling\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"unknown state is unknown result\",\n\t\t\tbuild: buildkite.Build{State: \"mystery\"},\n\t\t\twant:  Result{kind: resultUnknown, buildState: \"mystery\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := NewResult(tt.build); got != tt.want {\n\t\t\t\tt.Fatalf(\"NewResult() = %+v, want %+v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestResultPassed(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tbuild buildkite.Build\n\t\twant  bool\n\t}{\n\t\t{name: \"passed build\", build: buildkite.Build{State: \"passed\"}, want: true},\n\t\t{name: \"failed build\", build: buildkite.Build{State: \"failed\"}, want: false},\n\t\t{name: \"running build\", build: buildkite.Build{State: \"running\"}, want: false},\n\t\t{name: \"canceled build\", build: buildkite.Build{State: \"canceled\"}, want: false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := NewResult(tt.build).Passed(); got != tt.want {\n\t\t\t\tt.Fatalf(\"Passed() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestResultError(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tresult   Result\n\t\twantCode int\n\t\twantErr  bool\n\t\twantText string\n\t}{\n\t\t{name: \"clean pass\", result: Result{kind: resultCompletedPass, buildState: \"passed\"}, wantCode: bkErrors.ExitCodeSuccess},\n\t\t{name: \"completed failure\", result: Result{kind: resultCompletedFailure, buildState: \"failed\"}, wantCode: bkErrors.ExitCodePreflightCompletedFailure, wantErr: true, wantText: `preflight completed with failure: build is failed`},\n\t\t{name: \"incomplete failure\", result: Result{kind: resultIncompleteFailure, buildState: \"failing\"}, wantCode: bkErrors.ExitCodePreflightIncompleteFailure, wantErr: true, wantText: `preflight incomplete (failing): build is failing`},\n\t\t{name: \"incomplete\", result: Result{kind: resultIncomplete, buildState: \"blocked\"}, wantCode: bkErrors.ExitCodePreflightIncomplete, wantErr: true, wantText: `preflight incomplete: build is blocked`},\n\t\t{name: \"unknown state\", result: Result{kind: resultUnknown, buildState: \"passing\"}, wantCode: bkErrors.ExitCodePreflightUnknown, wantErr: true, wantText: `preflight result unknown: build is passing`},\n\t\t{name: \"unknown result kind\", result: Result{kind: resultKind(99), buildState: \"passed\"}, wantCode: bkErrors.ExitCodeInternalError, wantErr: true, wantText: `unexpected result kind 99 for build state 'passed', unable to coerce to error`},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.result.Error()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Fatalf(\"Result.Error() err presence = %v, wantErr %v\", err != nil, tt.wantErr)\n\t\t\t}\n\t\t\tif err == nil {\n\t\t\t\tif tt.wantCode != bkErrors.ExitCodeSuccess {\n\t\t\t\t\tt.Fatalf(\"Result.Error() = nil, want exit code %d\", tt.wantCode)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif code := bkErrors.GetExitCodeForError(err); code != tt.wantCode {\n\t\t\t\tt.Fatalf(\"Result.Error() exit code = %d, want %d\", code, tt.wantCode)\n\t\t\t}\n\t\t\tif tt.wantText != \"\" && !strings.Contains(err.Error(), tt.wantText) {\n\t\t\t\tt.Fatalf(\"Result.Error() text = %q, want substring %q\", err.Error(), tt.wantText)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/preflight/test_presenter.go",
    "content": "package preflight\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\tinternalpreflight \"github.com/buildkite/cli/v3/internal/preflight\"\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"github.com/mattn/go-runewidth\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype testPresenter struct{}\n\ntype summarySuiteColumnWidths struct {\n\tLabel   int\n\tFailed  int\n\tPassed  int\n\tSkipped int\n}\n\nfunc (p testPresenter) Line(t buildkite.BuildTest) string {\n\treturn p.line(t, false)\n}\n\nfunc (p testPresenter) ColoredLine(t buildkite.BuildTest) string {\n\treturn p.line(t, true)\n}\n\nfunc (p testPresenter) SummarySuiteLine(summary internalpreflight.SummaryTestRun, widths summarySuiteColumnWidths) string {\n\tlabel := padRightToWidth(summarySuiteLabel(summary.SuiteName, summary.SuiteSlug, \"unknown\"), widths.Label)\n\ticon := \"✓\"\n\tif summary.Failed > 0 {\n\t\ticon = \"✗\"\n\t}\n\n\treturn fmt.Sprintf(\n\t\t\"%s %s  %*d failed  %*d passed  %*d skipped\",\n\t\ticon,\n\t\tlabel,\n\t\twidths.Failed,\n\t\tsummary.Failed,\n\t\twidths.Passed,\n\t\tsummary.Passed,\n\t\twidths.Skipped,\n\t\tsummary.Skipped,\n\t)\n}\n\nfunc (p testPresenter) SummaryFailureLine(failure internalpreflight.SummaryTestFailure, width int, indent string) string {\n\tsuite := summarySuiteLabel(failure.SuiteName, failure.SuiteSlug, \"\")\n\tparts := make([]string, 0, 2)\n\tif location := strings.TrimSpace(failure.Location); location != \"\" {\n\t\tparts = append(parts, location)\n\t}\n\tif name := strings.TrimSpace(failure.Name); name != \"\" {\n\t\tparts = append(parts, truncateToWidth(name, 80))\n\t}\n\n\tline := \"✗\"\n\tif suite != \"\" {\n\t\tline += fmt.Sprintf(\" [%s]\", suite)\n\t}\n\tif len(parts) > 0 {\n\t\tline += \" \" + strings.Join(parts, \" — \")\n\t}\n\n\tif indent == \"\" {\n\t\tif width <= 0 {\n\t\t\treturn line\n\t\t}\n\t\treturn ansi.Hardwrap(line, width, false)\n\t}\n\n\tif width <= runewidth.StringWidth(indent) {\n\t\treturn indent + line\n\t}\n\n\tif width <= 0 {\n\t\treturn indent + line\n\t}\n\n\twrapped := ansi.Hardwrap(line, width-runewidth.StringWidth(indent), false)\n\tlines := strings.Split(wrapped, \"\\n\")\n\tfor i := range lines {\n\t\tlines[i] = indent + lines[i]\n\t}\n\n\treturn strings.Join(lines, \"\\n\")\n}\n\nfunc (p testPresenter) line(t buildkite.BuildTest, colored bool) string {\n\tname := t.Name\n\tif t.Scope != \"\" {\n\t\tname = t.Scope + \" \" + name\n\t}\n\tname = truncateToWidth(name, 80)\n\n\tlatestExecution := latestTestExecution(t)\n\n\tstatusIcon := formatTestStatusIcon(latestExecution, colored)\n\tline := fmt.Sprintf(\"%s %s\", statusIcon, name)\n\n\tif !isFailedTestExecution(latestExecution) {\n\t\treturn line\n\t}\n\n\tdetailParts := make([]string, 0, 2)\n\tif attemptSummary := testAttemptCounts(t); attemptSummary != \"\" {\n\t\tdetailParts = append(detailParts, attemptSummary)\n\t}\n\tif location := latestExecution.Location; location != \"\" {\n\t\tdetailParts = append(detailParts, location)\n\t} else if t.Location != \"\" {\n\t\tdetailParts = append(detailParts, t.Location)\n\t}\n\tif len(detailParts) > 0 {\n\t\tline += fmt.Sprintf(\"\\n    %s\", formatTestDetail(strings.Join(detailParts, \" — \"), colored))\n\t}\n\n\tif latestExecution.FailureReason != \"\" {\n\t\tline += fmt.Sprintf(\"\\n    %s\", formatTestDetail(latestExecution.FailureReason, colored))\n\t}\n\n\treturn line\n}\n\nfunc testAttemptCounts(t buildkite.BuildTest) string {\n\tattempts := t.ExecutionsCount\n\tif attempts == 0 {\n\t\treturn \"\"\n\t}\n\n\tpassed := t.ExecutionsCountByResult.Passed\n\tfailed := t.ExecutionsCountByResult.Failed\n\treturn fmt.Sprintf(\"%d %s (%d passed, %d failed)\", attempts, plural(attempts, \"attempt\"), passed, failed)\n}\n\nfunc latestTestExecution(t buildkite.BuildTest) *buildkite.BuildTestExecution {\n\texecutions := testExecutionsInTimestampOrder(t.Executions)\n\tif len(executions) == 0 {\n\t\treturn nil\n\t}\n\n\tlatest := executions[len(executions)-1]\n\tif latest.Timestamp == nil {\n\t\treturn nil\n\t}\n\n\treturn &latest\n}\n\nfunc testExecutionsInTimestampOrder(executions []buildkite.BuildTestExecution) []buildkite.BuildTestExecution {\n\tordered := append([]buildkite.BuildTestExecution(nil), executions...)\n\tsort.SliceStable(ordered, func(i, j int) bool {\n\t\tleft := ordered[i]\n\t\tright := ordered[j]\n\n\t\tswitch {\n\t\tcase left.Timestamp == nil && right.Timestamp == nil:\n\t\t\treturn false\n\t\tcase left.Timestamp == nil:\n\t\t\treturn true\n\t\tcase right.Timestamp == nil:\n\t\t\treturn false\n\t\tdefault:\n\t\t\treturn left.Timestamp.Before(right.Timestamp.Time)\n\t\t}\n\t})\n\n\treturn ordered\n}\n\nfunc isFailedTestExecution(execution *buildkite.BuildTestExecution) bool {\n\tif execution == nil {\n\t\treturn false\n\t}\n\n\treturn strings.EqualFold(execution.Status, \"failed\")\n}\n\nfunc formatTestDetail(text string, colored bool) string {\n\tif !colored {\n\t\treturn text\n\t}\n\n\treturn \"\\033[2m\" + text + \"\\033[0m\"\n}\n\nfunc formatTestStatusIcon(execution *buildkite.BuildTestExecution, colored bool) string {\n\tstatus := \"\"\n\tif execution != nil {\n\t\tstatus = execution.Status\n\t}\n\n\tif !colored {\n\t\tswitch {\n\t\tcase strings.EqualFold(status, \"passed\"):\n\t\t\treturn \"✓\"\n\t\tcase strings.EqualFold(status, \"failed\"):\n\t\t\treturn \"✗\"\n\t\tdefault:\n\t\t\treturn \"?\"\n\t\t}\n\t}\n\n\tswitch {\n\tcase strings.EqualFold(status, \"passed\"):\n\t\treturn \"\\033[32m✓\\033[0m\"\n\tcase strings.EqualFold(status, \"failed\"):\n\t\treturn \"\\033[31m✗\\033[0m\"\n\tdefault:\n\t\treturn \"\\033[2m?\\033[0m\"\n\t}\n}\n\nfunc truncateToWidth(s string, width int) string {\n\tif width <= 0 {\n\t\treturn \"\"\n\t}\n\n\tif runewidth.StringWidth(s) <= width {\n\t\treturn s\n\t}\n\n\tellipsis := \"...\"\n\tremaining := width - runewidth.StringWidth(ellipsis)\n\tif remaining <= 0 {\n\t\treturn ellipsis\n\t}\n\n\tleftWidth := remaining / 2\n\trightWidth := remaining - leftWidth\n\n\treturn trimLeftToWidth(s, leftWidth) + ellipsis + trimRightToWidth(s, rightWidth)\n}\n\nfunc trimLeftToWidth(s string, width int) string {\n\tvar b strings.Builder\n\tcurrentWidth := 0\n\n\tfor _, r := range s {\n\t\truneWidth := runewidth.RuneWidth(r)\n\t\tif currentWidth+runeWidth > width {\n\t\t\tbreak\n\t\t}\n\t\tb.WriteRune(r)\n\t\tcurrentWidth += runeWidth\n\t}\n\n\treturn b.String()\n}\n\nfunc trimRightToWidth(s string, width int) string {\n\trunes := []rune(s)\n\tcurrentWidth := 0\n\tstart := len(runes)\n\n\tfor start > 0 {\n\t\truneWidth := runewidth.RuneWidth(runes[start-1])\n\t\tif currentWidth+runeWidth > width {\n\t\t\tbreak\n\t\t}\n\t\tcurrentWidth += runeWidth\n\t\tstart--\n\t}\n\n\treturn string(runes[start:])\n}\n\nfunc padRightToWidth(s string, width int) string {\n\tpadding := width - runewidth.StringWidth(s)\n\tif padding <= 0 {\n\t\treturn s\n\t}\n\n\treturn s + strings.Repeat(\" \", padding)\n}\n"
  },
  {
    "path": "cmd/preflight/test_presenter_test.go",
    "content": "package preflight\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tinternalpreflight \"github.com/buildkite/cli/v3/internal/preflight\"\n)\n\nfunc TestTestPresenter_SummarySuiteLine(t *testing.T) {\n\tgot := testPresenter{}.SummarySuiteLine(\n\t\tinternalpreflight.SummaryTestRun{SuiteName: \"RSpec\", Passed: 47, Failed: 2, Skipped: 3},\n\t\tsummarySuiteColumnWidths{Label: 7, Failed: 1, Passed: 2, Skipped: 1},\n\t)\n\n\tif got != \"✗ RSpec    2 failed  47 passed  3 skipped\" {\n\t\tt.Fatalf(\"unexpected suite summary line: %q\", got)\n\t}\n}\n\nfunc TestTestPresenter_SummaryFailureLine_WrapsAndIndents(t *testing.T) {\n\tgot := testPresenter{}.SummaryFailureLine(internalpreflight.SummaryTestFailure{\n\t\tSuiteName: \"RSpec\",\n\t\tLocation:  \"src/auth.test.ts:89\",\n\t\tName:      \"AuthService.validateToken handles expired tokens and reports the reason cleanly\",\n\t\tMessage:   \"Expected 'expired' but got 'invalid' while validating the response payload\",\n\t}, 60, \"        \")\n\n\tlines := strings.Split(got, \"\\n\")\n\tif len(lines) < 2 {\n\t\tt.Fatalf(\"expected wrapped failure line, got %q\", got)\n\t}\n\n\tfor _, line := range lines {\n\t\tif !strings.HasPrefix(line, \"        \") {\n\t\t\tt.Fatalf(\"expected indented wrapped line, got %q\", line)\n\t\t}\n\t}\n\n\tif !strings.Contains(got, \"✗ [RSpec] src/auth.test.ts:89\") {\n\t\tt.Fatalf(\"expected suite-prefixed failure line, got %q\", got)\n\t}\n\tif strings.Contains(got, \"Expected 'expired' but got 'invalid'\") {\n\t\tt.Fatalf(\"expected summary failure line to omit failure message, got %q\", got)\n\t}\n}\n"
  },
  {
    "path": "cmd/preflight/tty.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/spinner\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\nvar (\n\tttyDimStyle         = lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\"))\n\tttyStatusStyle      = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#FFBA03\")).Bold(true)\n\tttyBorderStyle      = lipgloss.NewStyle().Foreground(lipgloss.Color(\"238\"))\n\tttyFailureStyle     = lipgloss.NewStyle().Foreground(lipgloss.Color(\"9\"))\n\tttySoftFailureStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(\"11\"))\n)\n\ntype ttyModel struct {\n\tspinner    spinner.Model\n\tlatest     Event\n\tsummary    *Event\n\tcancelFunc context.CancelFunc\n\twidth      int\n}\n\nfunc newTTYModel() ttyModel {\n\ts := spinner.New()\n\ts.Spinner = spinner.Dot\n\ts.Style = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#DE8F0C\"))\n\treturn ttyModel{spinner: s}\n}\n\nfunc (m ttyModel) Init() tea.Cmd {\n\treturn m.spinner.Tick\n}\n\nfunc (m ttyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"q\", \"ctrl+c\":\n\t\t\tif m.cancelFunc != nil {\n\t\t\t\tm.cancelFunc()\n\t\t\t}\n\t\t\treturn m, nil\n\t\t}\n\n\tcase Event:\n\t\tswitch msg.Type {\n\t\tcase EventOperation:\n\t\t\tm.latest = msg\n\t\t\ttimestamp := ttyDimStyle.Render(msg.Time.Format(\"15:04:05\"))\n\t\t\tprefix := timestamp + \" \"\n\t\t\tline := prefix + msg.Title\n\t\t\tif msg.Detail != \"\" {\n\t\t\t\tdetail := indentAllLines(msg.Detail, len(\"15:04:05 \"))\n\t\t\t\tline += \":\\n\" + detail\n\t\t\t}\n\t\t\treturn m, tea.Printf(\"%s\", m.hardwrapLine(line))\n\n\t\tcase EventBuildStatus:\n\t\t\tm.latest = msg\n\t\t\treturn m, nil\n\n\t\tcase EventJobFailure:\n\t\t\tif msg.Job != nil {\n\t\t\t\tpresenter := jobPresenter{pipeline: msg.Pipeline, buildNumber: msg.BuildNumber, buildURL: msg.BuildURL}\n\t\t\t\tline := timestampPrefix(msg.Time) + presenter.ColoredLine(*msg.Job)\n\t\t\t\treturn m, tea.Printf(\"%s\", m.hardwrapLine(line))\n\t\t\t}\n\n\t\tcase EventJobRetryPassed:\n\t\t\tif msg.Job != nil {\n\t\t\t\tpresenter := jobPresenter{pipeline: msg.Pipeline, buildNumber: msg.BuildNumber, buildURL: msg.BuildURL}\n\t\t\t\tline := timestampPrefix(msg.Time) + presenter.ColoredRetryPassedLine(*msg.Job)\n\t\t\t\treturn m, tea.Printf(\"%s\", m.hardwrapLine(line))\n\t\t\t}\n\n\t\tcase EventBuildSummary:\n\t\t\t// Print the summary via Printf (which scrolls it above the\n\t\t\t// view) instead of rendering it through View(). Inline-image\n\t\t\t// escape sequences from emoji.Render confuse Bubbletea's\n\t\t\t// cursor tracking, causing lines to vanish on re-render.\n\t\t\tm.summary = &msg\n\t\t\treturn m, tea.Sequence(\n\t\t\t\ttea.Printf(\"%s\", buildSummaryView(msg, m.width)),\n\t\t\t\ttea.Quit,\n\t\t\t)\n\n\t\tcase EventTestFailure:\n\t\t\tif len(msg.TestFailures) > 0 {\n\t\t\t\tpresenter := testPresenter{}\n\t\t\t\tvar cmds []tea.Cmd\n\t\t\t\tfor _, t := range msg.TestFailures {\n\t\t\t\t\tline := formatTimestampedBlock(presenter.ColoredLine(t), msg.Time)\n\t\t\t\t\tcmds = append(cmds, tea.Printf(\"%s\", m.hardwrapLine(line)))\n\t\t\t\t}\n\t\t\t\treturn m, tea.Batch(cmds...)\n\t\t\t}\n\t\t}\n\n\tcase tea.WindowSizeMsg:\n\t\tm.width = msg.Width\n\t\treturn m, nil\n\n\tcase spinner.TickMsg:\n\t\tvar cmd tea.Cmd\n\t\tm.spinner, cmd = m.spinner.Update(msg)\n\t\treturn m, cmd\n\t}\n\n\treturn m, nil\n}\n\nfunc (m ttyModel) statusText() string {\n\tswitch {\n\tcase m.latest.Title != \"\":\n\t\treturn m.latest.Title\n\tcase m.latest.BuildState != \"\":\n\t\tlink := terminalHyperlink(fmt.Sprintf(\"build #%d\", m.latest.BuildNumber), m.latest.BuildURL)\n\t\treturn fmt.Sprintf(\"Watching %s (%s)\", link, m.latest.BuildState)\n\tdefault:\n\t\treturn \"Starting...\"\n\t}\n}\n\n// hardwrapLine pre-wraps text with explicit newlines at the terminal width so that\n// Bubbletea's line counting matches the physical rows the terminal will use.\n// This prevents cursor positioning errors that leave View() artifacts in the scrollback.\nfunc (m ttyModel) hardwrapLine(s string) string {\n\tif m.width <= 0 {\n\t\treturn s\n\t}\n\treturn ansi.Hardwrap(s, m.width, false)\n}\n\nfunc (m ttyModel) render() string {\n\tseparator := ttyBorderStyle.Render(\"─────────────────────────────────────────────\")\n\n\tstatusLine := fmt.Sprintf(\"  %s %s\", m.spinner.View(), ttyStatusStyle.Render(m.statusText()))\n\n\tif m.latest.Jobs == nil {\n\t\treturn separator + \"\\n\" + statusLine\n\t}\n\n\tparts := make([]string, 0, 6)\n\tappendPart := func(count int, text string) {\n\t\tif count > 0 {\n\t\t\tparts = append(parts, text)\n\t\t}\n\t}\n\tappendPart(m.latest.Jobs.Passed, fmt.Sprintf(\"%d passed\", m.latest.Jobs.Passed))\n\tappendPart(m.latest.Jobs.Failed, ttyFailureStyle.Render(fmt.Sprintf(\"%d failed\", m.latest.Jobs.Failed)))\n\tappendPart(m.latest.Jobs.SoftFailed, ttySoftFailureStyle.Render(fmt.Sprintf(\"%d soft failed\", m.latest.Jobs.SoftFailed)))\n\tappendPart(m.latest.Jobs.Running, fmt.Sprintf(\"%d running\", m.latest.Jobs.Running))\n\tappendPart(m.latest.Jobs.Scheduled, fmt.Sprintf(\"%d scheduled\", m.latest.Jobs.Scheduled))\n\tappendPart(m.latest.Jobs.Waiting, fmt.Sprintf(\"%d waiting\", m.latest.Jobs.Waiting))\n\n\tif len(parts) == 0 {\n\t\treturn separator + \"\\n\" + statusLine\n\t}\n\n\tsummaryLine := fmt.Sprintf(\"  %s\", ttyDimStyle.Render(strings.Join(parts, \", \")))\n\treturn separator + \"\\n\" + statusLine + \"\\n\" + summaryLine\n}\n\nfunc (m ttyModel) View() string {\n\tif m.summary != nil {\n\t\t// Summary was already printed via tea.Printf; return empty\n\t\t// so Bubbletea clears the spinner area on exit.\n\t\treturn \"\"\n\t}\n\treturn m.hardwrapLine(m.render())\n}\n\n// buildSummaryView renders the final build summary for TTY output.\nfunc buildSummaryView(e Event, width int) string {\n\tstyle := ttyFailureStyle\n\tif e.BuildState == \"passed\" {\n\t\tstyle = ttyStatusStyle\n\t}\n\n\tseparator := ttyBorderStyle.Render(\"─────────────────────────────────────────────\")\n\tout := separator + \"\\n\" + style.Render(summaryHeader(e))\n\tif label := summaryBuildLabel(e); label != \"\" && e.BuildURL != \"\" {\n\t\tout += \"\\n  \" + ttyDimStyle.Render(label)\n\t\tbuildURL := \"  \" + ttyDimStyle.Render(e.BuildURL)\n\t\tif width > 0 {\n\t\t\tbuildURL = ansi.Hardwrap(buildURL, width, false)\n\t\t}\n\t\tout += \"\\n\" + buildURL\n\t}\n\n\tpresenter := jobPresenter{pipeline: e.Pipeline, buildNumber: e.BuildNumber, buildURL: e.BuildURL}\n\tfor _, j := range e.PassedJobs {\n\t\tout += \"\\n  \" + presenter.ColoredPassedLine(j, ttyDimStyle)\n\t}\n\tout += buildSummaryDetails(e, true, width)\n\n\treturn out\n}\n\ntype ttyRenderer struct {\n\tprogram *tea.Program\n\tdone    chan struct{}\n\terr     error\n}\n\nfunc newTTYRenderer(cancel context.CancelFunc) *ttyRenderer {\n\tmodel := newTTYModel()\n\tmodel.cancelFunc = cancel\n\tp := tea.NewProgram(model)\n\tr := &ttyRenderer{program: p, done: make(chan struct{})}\n\tgo func() {\n\t\tif _, err := p.Run(); err != nil {\n\t\t\tr.err = err\n\t\t}\n\t\tclose(r.done)\n\t}()\n\treturn r\n}\n\nfunc (r *ttyRenderer) Render(e Event) error {\n\tr.program.Send(e)\n\treturn nil\n}\n\nfunc (r *ttyRenderer) Close() error {\n\tr.program.Quit()\n\t<-r.done\n\treturn r.err\n}\n"
  },
  {
    "path": "cmd/preflight/tty_test.go",
    "content": "package preflight\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tinternalpreflight \"github.com/buildkite/cli/v3/internal/preflight\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestBuildSummaryView_ReturnsOutput(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\twidth    int\n\t\tevent    Event\n\t\tcontains []string\n\t}{\n\t\t{\n\t\t\tname:  \"passed build no jobs\",\n\t\t\twidth: 0,\n\t\t\tevent: Event{\n\t\t\t\tType:        EventBuildSummary,\n\t\t\t\tBuildState:  \"passed\",\n\t\t\t\tBuildNumber: 42,\n\t\t\t\tBuildURL:    \"https://buildkite.com/buildkite/cli/builds/42\",\n\t\t\t},\n\t\t\tcontains: []string{\"─────\", \"Build #42\", \"https://buildkite.com/buildkite/cli/builds/42\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"passed build with jobs\",\n\t\t\twidth: 0,\n\t\t\tevent: Event{\n\t\t\t\tType:        EventBuildSummary,\n\t\t\t\tBuildState:  \"passed\",\n\t\t\t\tBuildNumber: 42,\n\t\t\t\tBuildURL:    \"https://buildkite.com/buildkite/cli/builds/42\",\n\t\t\t\tPassedJobs: []buildkite.Job{\n\t\t\t\t\t{ID: \"j1\", Name: \"Lint\", Type: \"script\", State: \"passed\"},\n\t\t\t\t\t{ID: \"j2\", Name: \"Test\", Type: \"script\", State: \"passed\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\"Build #42\", \"https://buildkite.com/buildkite/cli/builds/42\", \"✔ Lint\", \"✔ Test\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"failed build no jobs\",\n\t\t\twidth: 0,\n\t\t\tevent: Event{\n\t\t\t\tType:        EventBuildSummary,\n\t\t\t\tBuildState:  \"failed\",\n\t\t\t\tBuildNumber: 42,\n\t\t\t\tBuildURL:    \"https://buildkite.com/buildkite/cli/builds/42\",\n\t\t\t},\n\t\t\tcontains: []string{\"─────\", \"Build #42\", \"https://buildkite.com/buildkite/cli/builds/42\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"failed build with jobs\",\n\t\t\twidth: 0,\n\t\t\tevent: Event{\n\t\t\t\tType:        EventBuildSummary,\n\t\t\t\tBuildState:  \"failed\",\n\t\t\t\tPipeline:    \"buildkite/cli\",\n\t\t\t\tBuildNumber: 42,\n\t\t\t\tBuildURL:    \"https://buildkite.com/buildkite/cli/builds/42\",\n\t\t\t\tFailedJobs: func() []buildkite.Job {\n\t\t\t\t\texit := 1\n\t\t\t\t\treturn []buildkite.Job{\n\t\t\t\t\t\t{ID: \"j1\", Name: \"Lint\", Type: \"script\", State: \"failed\", ExitStatus: &exit},\n\t\t\t\t\t}\n\t\t\t\t}(),\n\t\t\t},\n\t\t\tcontains: []string{\"Build #42\", \"https://buildkite.com/buildkite/cli/builds/42\", \"✗\", \"Lint\", \"failed with exit 1\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"build stopped early\",\n\t\t\twidth: 0,\n\t\t\tevent: Event{\n\t\t\t\tType:        EventBuildSummary,\n\t\t\t\tBuildState:  \"failing\",\n\t\t\t\tBuildNumber: 42,\n\t\t\t\tBuildURL:    \"https://buildkite.com/buildkite/cli/builds/42\",\n\t\t\t\tIncomplete:  true,\n\t\t\t\tStopReason:  \"build-failing\",\n\t\t\t},\n\t\t\tcontains: []string{\"Preflight Incomplete (build failing)\", \"Build #42\", \"https://buildkite.com/buildkite/cli/builds/42\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"wraps build url on narrow terminals\",\n\t\t\twidth: 24,\n\t\t\tevent: Event{\n\t\t\t\tType:        EventBuildSummary,\n\t\t\t\tBuildState:  \"passed\",\n\t\t\t\tBuildNumber: 42,\n\t\t\t\tBuildURL:    \"https://buildkite.com/buildkite/cli/builds/42\",\n\t\t\t},\n\t\t\tcontains: []string{\"Build #42\", \"https://buildkite.com/\", \"buildkite/cli/builds/42\"},\n\t\t},\n\t\t{\n\t\t\tname: \"build with tests\",\n\t\t\tevent: Event{\n\t\t\t\tType:       EventBuildSummary,\n\t\t\t\tBuildState: \"failed\",\n\t\t\t\tTests: internalpreflight.SummaryTests{\n\t\t\t\t\tRuns: map[string]internalpreflight.SummaryTestRun{\n\t\t\t\t\t\t\"run-rspec\": {RunID: \"run-rspec\", SuiteName: \"RSpec\", SuiteSlug: \"rspec\", Passed: 47, Failed: 2, Skipped: 3},\n\t\t\t\t\t},\n\t\t\t\t\tFailures: []internalpreflight.SummaryTestFailure{{\n\t\t\t\t\t\tRunID:     \"run-rspec\",\n\t\t\t\t\t\tSuiteName: \"RSpec\",\n\t\t\t\t\t\tSuiteSlug: \"rspec\",\n\t\t\t\t\t\tName:      \"AuthService.validateToken handles expired tokens\",\n\t\t\t\t\t\tLocation:  \"src/auth.test.ts:89\",\n\t\t\t\t\t\tMessage:   \"Expected 'expired' but got 'invalid'\",\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcontains: []string{\"Tests Failed ✗\", \"✗ RSpec  2 failed  47 passed  3 skipped\", \"✗ [RSpec] src/auth.test.ts:89 — AuthService.validateToken handles expired tokens\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := buildSummaryView(tt.event, tt.width)\n\t\t\tif got == \"\" {\n\t\t\t\tt.Fatal(\"expected non-empty summary view\")\n\t\t\t}\n\t\t\tfor _, want := range tt.contains {\n\t\t\t\tif !strings.Contains(got, want) {\n\t\t\t\t\tt.Errorf(\"missing %q in output:\\n%s\", want, got)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/queue/create.go",
    "content": "package queue\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype CreateCmd struct {\n\tClusterUUID        string `arg:\"\" help:\"Cluster UUID to create the queue in\" name:\"cluster-uuid\"`\n\tKey                string `help:\"A unique key for the queue\" required:\"\"`\n\tDescription        string `help:\"A description of the queue\" optional:\"\"`\n\tRetryAgentAffinity string `help:\"Retry agent affinity setting (prefer-warmest or prefer-different)\" optional:\"\" name:\"retry-agent-affinity\"`\n\toutput.OutputFlags\n}\n\nfunc (c *CreateCmd) Validate() error {\n\tswitch buildkite.RetryAgentAffinity(c.RetryAgentAffinity) {\n\tcase \"\", buildkite.RetryAgentAffinityPreferWarmest, buildkite.RetryAgentAffinityPreferDifferent:\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\n\t\t\t\"invalid --retry-agent-affinity value %q: must be %s or %s\",\n\t\t\tc.RetryAgentAffinity,\n\t\t\tbuildkite.RetryAgentAffinityPreferWarmest,\n\t\t\tbuildkite.RetryAgentAffinityPreferDifferent,\n\t\t)\n\t}\n}\n\nfunc (c *CreateCmd) Help() string {\n\treturn `\nExamples:\n  # Create a queue with just a key\n  $ bk queue create my-cluster-uuid --key my-queue\n\n  # Create a queue with a description\n  $ bk queue create my-cluster-uuid --key my-queue --description \"My new queue\"\n\n  # Create a queue with retry agent affinity set\n  $ bk queue create my-cluster-uuid --key my-queue --retry-agent-affinity prefer-different\n\n  # Create a queue and output as JSON\n  $ bk queue create my-cluster-uuid --key my-queue -o json\n`\n}\n\nfunc (c *CreateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tinput := buildkite.ClusterQueueCreate{\n\t\tKey:         c.Key,\n\t\tDescription: c.Description,\n\t}\n\tif c.RetryAgentAffinity != \"\" {\n\t\tinput.RetryAgentAffinity = buildkite.RetryAgentAffinity(c.RetryAgentAffinity)\n\t}\n\n\tvar queue buildkite.ClusterQueue\n\tif err = bkIO.SpinWhile(f, \"Creating cluster queue\", func() error {\n\t\tvar apiErr error\n\t\tqueue, _, apiErr = f.RestAPIClient.ClusterQueues.Create(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, input)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error creating cluster queue: %w\", err)\n\t}\n\n\tqueueView := output.Viewable[buildkite.ClusterQueue]{\n\t\tData:   queue,\n\t\tRender: renderQueueText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, queueView, format)\n\t}\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\tfmt.Fprintf(writer, \"Queue %s created successfully\\n\\n\", queue.Key)\n\treturn output.Write(writer, queueView, format)\n}\n"
  },
  {
    "path": "cmd/queue/delete.go",
    "content": "package queue\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n)\n\ntype DeleteCmd struct {\n\tClusterUUID string `arg:\"\" help:\"Cluster UUID the queue belongs to\" name:\"cluster-uuid\"`\n\tQueueUUID   string `arg:\"\" help:\"Queue UUID to delete\" name:\"queue-uuid\"`\n}\n\nfunc (c *DeleteCmd) Help() string {\n\treturn `\nYou will be prompted to confirm deletion unless --yes is set.\n\nExamples:\n  # Delete a queue\n  $ bk queue delete my-cluster-uuid my-queue-uuid\n\n  # Delete a queue without confirmation\n  $ bk queue delete my-cluster-uuid my-queue-uuid --yes\n`\n}\n\nfunc (c *DeleteCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tconfirmed, err := bkIO.Confirm(f, fmt.Sprintf(\"Are you sure you want to delete queue %s?\", c.QueueUUID))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !confirmed {\n\t\tfmt.Fprintln(os.Stderr, \"Deletion cancelled.\")\n\t\treturn nil\n\t}\n\n\tif err = bkIO.SpinWhile(f, \"Deleting cluster queue\", func() error {\n\t\t_, apiErr := f.RestAPIClient.ClusterQueues.Delete(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, c.QueueUUID)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error deleting cluster queue: %w\", err)\n\t}\n\n\tfmt.Fprintf(os.Stderr, \"Queue %s deleted successfully.\\n\", c.QueueUUID)\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/queue/list.go",
    "content": "package queue\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype ListCmd struct {\n\tClusterUUID string `arg:\"\" help:\"Cluster UUID to list queues for\" name:\"cluster-uuid\"`\n\tPerPage     int    `help:\"Number of queues per page\" default:\"30\"`\n\tLimit       int    `help:\"Maximum number of queues to return\" default:\"100\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ListCmd) Validate() error {\n\tif c.PerPage < 1 {\n\t\treturn fmt.Errorf(\"invalid --per-page %d: must be greater than 0\", c.PerPage)\n\t}\n\n\tif c.Limit < 0 {\n\t\treturn fmt.Errorf(\"invalid --limit %d: must be greater than or equal to 0\", c.Limit)\n\t}\n\n\treturn nil\n}\n\nfunc (c *ListCmd) Help() string {\n\treturn `\nExamples:\n  # List all queues for a cluster\n  $ bk queue list my-cluster-uuid\n\n  # Return more queues\n  $ bk queue list my-cluster-uuid --limit 200\n\n  # List in JSON format\n  $ bk queue list my-cluster-uuid -o json\n`\n}\n\nfunc (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tvar queues []buildkite.ClusterQueue\n\tpage := 1\n\tvar previousFirstQueueID string\n\n\tfor len(queues) < c.Limit {\n\t\topts := &buildkite.ClusterQueuesListOptions{\n\t\t\tListOptions: buildkite.ListOptions{\n\t\t\t\tPage:    page,\n\t\t\t\tPerPage: c.PerPage,\n\t\t\t},\n\t\t}\n\n\t\tvar pageQueues []buildkite.ClusterQueue\n\t\tif err := bkIO.SpinWhile(f, \"Fetching cluster queues\", func() error {\n\t\t\tvar apiErr error\n\t\t\tpageQueues, _, apiErr = f.RestAPIClient.ClusterQueues.List(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, opts)\n\t\t\treturn apiErr\n\t\t}); err != nil {\n\t\t\treturn fmt.Errorf(\"error fetching cluster queues: %w\", err)\n\t\t}\n\n\t\tif len(pageQueues) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tif page > 1 && pageQueues[0].ID == previousFirstQueueID {\n\t\t\treturn fmt.Errorf(\"API returned duplicate page content at page %d, stopping pagination to prevent infinite loop\", page)\n\t\t}\n\t\tpreviousFirstQueueID = pageQueues[0].ID\n\n\t\tqueues = append(queues, pageQueues...)\n\n\t\tif len(pageQueues) < c.PerPage {\n\t\t\tbreak\n\t\t}\n\n\t\tif len(queues) >= c.Limit {\n\t\t\tbreak\n\t\t}\n\n\t\tpage++\n\t}\n\n\tif len(queues) > c.Limit {\n\t\tqueues = queues[:c.Limit]\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, queues, format)\n\t}\n\n\tif len(queues) == 0 {\n\t\tfmt.Fprintln(os.Stdout, \"No queues found\")\n\t\treturn nil\n\t}\n\n\trows := make([][]string, 0, len(queues))\n\tfor _, q := range queues {\n\t\tpaused := \"No\"\n\t\tif q.DispatchPaused {\n\t\t\tpaused = \"Yes\"\n\t\t}\n\t\trows = append(rows, []string{q.Key, output.ValueOrDash(q.Description), paused, q.ID})\n\t}\n\n\ttable := output.Table(\n\t\t[]string{\"Key\", \"Description\", \"Paused\", \"ID\"},\n\t\trows,\n\t\tmap[string]string{\"key\": \"bold\"},\n\t)\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\t_, err = fmt.Fprintf(writer, \"Queues (%d)\\n\\n%s\\n\", len(queues), table)\n\treturn err\n}\n"
  },
  {
    "path": "cmd/queue/pause.go",
    "content": "package queue\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype PauseCmd struct {\n\tClusterUUID string `arg:\"\" help:\"Cluster UUID the queue belongs to\" name:\"cluster-uuid\"`\n\tQueueUUID   string `arg:\"\" help:\"Queue UUID to pause\" name:\"queue-uuid\"`\n\tNote        string `help:\"Optional note explaining why the queue is being paused\" optional:\"\" name:\"note\"`\n\toutput.OutputFlags\n}\n\nfunc (c *PauseCmd) Help() string {\n\treturn `\nThe queue remains paused until it is resumed with \"bk queue resume\".\n\nExamples:\n  # Pause a queue\n  $ bk queue pause my-cluster-uuid my-queue-uuid\n\n  # Pause a queue with a note\n  $ bk queue pause my-cluster-uuid my-queue-uuid --note \"Pausing for maintenance\"\n\n  # Output the paused queue as JSON\n  $ bk queue pause my-cluster-uuid my-queue-uuid --note \"Maintenance\" -o json\n`\n}\n\nfunc (c *PauseCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tinput := buildkite.ClusterQueuePause{\n\t\tNote: c.Note,\n\t}\n\n\tvar queue buildkite.ClusterQueue\n\tif err = bkIO.SpinWhile(f, \"Pausing cluster queue\", func() error {\n\t\tvar apiErr error\n\t\tqueue, _, apiErr = f.RestAPIClient.ClusterQueues.Pause(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, c.QueueUUID, input)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error pausing cluster queue: %w\", err)\n\t}\n\n\tqueueView := output.Viewable[buildkite.ClusterQueue]{\n\t\tData:   queue,\n\t\tRender: renderQueueText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, queueView, format)\n\t}\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\tfmt.Fprintf(writer, \"Queue %s paused successfully\\n\\n\", queue.Key)\n\treturn output.Write(writer, queueView, format)\n}\n"
  },
  {
    "path": "cmd/queue/queue_test.go",
    "content": "package queue\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc newTestQueue(key, id string, paused bool) buildkite.ClusterQueue {\n\treturn buildkite.ClusterQueue{\n\t\tID:             id,\n\t\tKey:            key,\n\t\tDescription:    \"Test queue\",\n\t\tDispatchPaused: paused,\n\t\tCreatedAt:      &buildkite.Timestamp{Time: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)},\n\t}\n}\n\nfunc TestCmdQueueList(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"validates per-page and limit\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttests := []struct {\n\t\t\tname    string\n\t\t\tperPage int\n\t\t\tlimit   int\n\t\t\twantErr bool\n\t\t}{\n\t\t\t{\"valid defaults\", 30, 100, false},\n\t\t\t{\"per-page zero invalid\", 0, 100, true},\n\t\t\t{\"per-page negative invalid\", -1, 100, true},\n\t\t\t{\"limit zero valid\", 30, 0, false},\n\t\t\t{\"limit negative invalid\", 30, -1, true},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\t\t\t\tcmd := &ListCmd{PerPage: tt.perPage, Limit: tt.limit}\n\t\t\t\terr := cmd.Validate()\n\t\t\t\tif tt.wantErr && err == nil {\n\t\t\t\t\tt.Error(\"expected error, got nil\")\n\t\t\t\t}\n\t\t\t\tif !tt.wantErr && err != nil {\n\t\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"fetches queues through API\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tqueues := []buildkite.ClusterQueue{\n\t\t\tnewTestQueue(\"default\", \"queue-1\", false),\n\t\t\tnewTestQueue(\"deploy\", \"queue-2\", true),\n\t\t}\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tpage := r.URL.Query().Get(\"page\")\n\t\t\tif page == \"\" || page == \"1\" {\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\tjson.NewEncoder(w).Encode(queues)\n\t\t\t} else {\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.ClusterQueue{})\n\t\t\t}\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgot, _, err := client.ClusterQueues.List(ctx, \"test-org\", \"cluster-1\", &buildkite.ClusterQueuesListOptions{\n\t\t\tListOptions: buildkite.ListOptions{Page: 1, PerPage: 30},\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(got) != 2 {\n\t\t\tt.Fatalf(\"expected 2 queues, got %d\", len(got))\n\t\t}\n\t\tif got[0].Key != \"default\" {\n\t\t\tt.Errorf(\"expected first key 'default', got %q\", got[0].Key)\n\t\t}\n\t\tif !got[1].DispatchPaused {\n\t\t\tt.Error(\"expected second queue to be paused\")\n\t\t}\n\t})\n\n\tt.Run(\"empty result returns empty slice\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode([]buildkite.ClusterQueue{})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgot, _, err := client.ClusterQueues.List(ctx, \"test-org\", \"cluster-1\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif len(got) != 0 {\n\t\t\tt.Errorf(\"expected 0 queues, got %d\", len(got))\n\t\t}\n\t})\n}\n\nfunc TestCmdQueueCreate(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"validates retry agent affinity\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttests := []struct {\n\t\t\tname     string\n\t\t\taffinity string\n\t\t\twantErr  bool\n\t\t}{\n\t\t\t{\"empty affinity valid\", \"\", false},\n\t\t\t{\"prefer-warmest valid\", string(buildkite.RetryAgentAffinityPreferWarmest), false},\n\t\t\t{\"prefer-different valid\", string(buildkite.RetryAgentAffinityPreferDifferent), false},\n\t\t\t{\"invalid affinity\", \"prefer-random\", true},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\t\t\t\tcmd := &CreateCmd{ClusterUUID: \"cluster-1\", Key: \"my-queue\", RetryAgentAffinity: tt.affinity}\n\t\t\t\terr := cmd.Validate()\n\t\t\t\tif tt.wantErr && err == nil {\n\t\t\t\t\tt.Error(\"expected error, got nil\")\n\t\t\t\t}\n\t\t\t\tif !tt.wantErr && err != nil {\n\t\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"creates queue through API\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tqueue := newTestQueue(\"my-queue\", \"queue-abc\", false)\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != http.MethodPost {\n\t\t\t\tt.Errorf(\"expected POST, got %s\", r.Method)\n\t\t\t}\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tw.WriteHeader(http.StatusCreated)\n\t\t\tjson.NewEncoder(w).Encode(queue)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgot, _, err := client.ClusterQueues.Create(ctx, \"test-org\", \"cluster-1\", buildkite.ClusterQueueCreate{\n\t\t\tKey:         \"my-queue\",\n\t\t\tDescription: \"Test queue\",\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif got.Key != \"my-queue\" {\n\t\t\tt.Errorf(\"expected key 'my-queue', got %q\", got.Key)\n\t\t}\n\t})\n}\n\nfunc TestCmdQueueUpdate(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"requires at least one field\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttests := []struct {\n\t\t\tname        string\n\t\t\tdescription string\n\t\t\taffinity    string\n\t\t\twantErr     bool\n\t\t}{\n\t\t\t{\"no fields provided\", \"\", \"\", true},\n\t\t\t{\"description only\", \"new desc\", \"\", false},\n\t\t\t{\"affinity only valid\", string(buildkite.RetryAgentAffinityPreferWarmest), \"\", false},\n\t\t\t{\"both fields\", \"new desc\", string(buildkite.RetryAgentAffinityPreferDifferent), false},\n\t\t\t{\"invalid affinity\", \"\", \"bad-value\", true},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\t\t\t\tcmd := &UpdateCmd{\n\t\t\t\t\tClusterUUID:        \"cluster-1\",\n\t\t\t\t\tQueueUUID:          \"queue-1\",\n\t\t\t\t\tDescription:        tt.description,\n\t\t\t\t\tRetryAgentAffinity: tt.affinity,\n\t\t\t\t}\n\t\t\t\terr := cmd.Validate()\n\t\t\t\tif tt.wantErr && err == nil {\n\t\t\t\t\tt.Error(\"expected error, got nil\")\n\t\t\t\t}\n\t\t\t\tif !tt.wantErr && err != nil {\n\t\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"updates queue through API\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tupdated := newTestQueue(\"my-queue\", \"queue-abc\", false)\n\t\tupdated.Description = \"Updated description\"\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != http.MethodPatch {\n\t\t\t\tt.Errorf(\"expected PATCH, got %s\", r.Method)\n\t\t\t}\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(updated)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgot, _, err := client.ClusterQueues.Update(ctx, \"test-org\", \"cluster-1\", \"queue-abc\", buildkite.ClusterQueueUpdate{\n\t\t\tDescription: \"Updated description\",\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif got.Description != \"Updated description\" {\n\t\t\tt.Errorf(\"expected description 'Updated description', got %q\", got.Description)\n\t\t}\n\t})\n}\n\nfunc TestCmdQueueDelete(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"sends DELETE request\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tcalled := false\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tcalled = true\n\t\t\tif r.Method != http.MethodDelete {\n\t\t\t\tt.Errorf(\"expected DELETE, got %s\", r.Method)\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tctx := context.Background()\n\t\t_, err = client.ClusterQueues.Delete(ctx, \"test-org\", \"cluster-1\", \"queue-abc\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !called {\n\t\t\tt.Error(\"expected DELETE to be called\")\n\t\t}\n\t})\n}\n\nfunc TestCmdQueuePause(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"sends POST to pause_dispatch with note\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tqueue := newTestQueue(\"my-queue\", \"queue-abc\", true)\n\t\tqueue.DispatchPausedNote = \"maintenance\"\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != http.MethodPost {\n\t\t\t\tt.Errorf(\"expected POST, got %s\", r.Method)\n\t\t\t}\n\t\t\tif !strings.HasSuffix(r.URL.Path, \"/pause_dispatch\") {\n\t\t\t\tt.Errorf(\"expected path to end in /pause_dispatch, got %q\", r.URL.Path)\n\t\t\t}\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(queue)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tctx := context.Background()\n\t\tgot, _, err := client.ClusterQueues.Pause(ctx, \"test-org\", \"cluster-1\", \"queue-abc\", buildkite.ClusterQueuePause{\n\t\t\tNote: \"maintenance\",\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !got.DispatchPaused {\n\t\t\tt.Error(\"expected queue to be paused\")\n\t\t}\n\t\tif got.DispatchPausedNote != \"maintenance\" {\n\t\t\tt.Errorf(\"expected note 'maintenance', got %q\", got.DispatchPausedNote)\n\t\t}\n\t})\n}\n\nfunc TestCmdQueueResume(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"sends POST to resume_dispatch\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tcalled := false\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tcalled = true\n\t\t\tif r.Method != http.MethodPost {\n\t\t\t\tt.Errorf(\"expected POST, got %s\", r.Method)\n\t\t\t}\n\t\t\tif !strings.HasSuffix(r.URL.Path, \"/resume_dispatch\") {\n\t\t\t\tt.Errorf(\"expected path to end in /resume_dispatch, got %q\", r.URL.Path)\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tctx := context.Background()\n\t\t_, err = client.ClusterQueues.Resume(ctx, \"test-org\", \"cluster-1\", \"queue-abc\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !called {\n\t\t\tt.Error(\"expected resume_dispatch to be called\")\n\t\t}\n\t})\n}\n\nfunc TestRenderQueueText(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"unpaused queue\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tq := newTestQueue(\"my-queue\", \"queue-abc\", false)\n\t\tout := renderQueueText(q)\n\t\tif !strings.Contains(out, \"my-queue\") {\n\t\t\tt.Error(\"expected output to contain queue key\")\n\t\t}\n\t\tif !strings.Contains(out, \"No\") {\n\t\t\tt.Error(\"expected output to show paused as 'No'\")\n\t\t}\n\t})\n\n\tt.Run(\"paused queue with note\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tq := newTestQueue(\"my-queue\", \"queue-abc\", true)\n\t\tq.DispatchPausedNote = \"maintenance window\"\n\t\tout := renderQueueText(q)\n\t\tif !strings.Contains(out, \"Yes\") {\n\t\t\tt.Error(\"expected output to show paused as 'Yes'\")\n\t\t}\n\t\tif !strings.Contains(out, \"maintenance window\") {\n\t\t\tt.Error(\"expected output to contain pause note\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "cmd/queue/resume.go",
    "content": "package queue\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype ResumeCmd struct {\n\tClusterUUID string `arg:\"\" help:\"Cluster UUID the queue belongs to\" name:\"cluster-uuid\"`\n\tQueueUUID   string `arg:\"\" help:\"Queue UUID to resume\" name:\"queue-uuid\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ResumeCmd) Help() string {\n\treturn `\nResumes dispatch for a paused cluster queue.\n\nExamples:\n  # Resume a queue\n  $ bk queue resume my-cluster-uuid my-queue-uuid\n\n  # Output the resumed queue as JSON\n  $ bk queue resume my-cluster-uuid my-queue-uuid -o json\n`\n}\n\nfunc (c *ResumeCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tif err = bkIO.SpinWhile(f, \"Resuming cluster queue\", func() error {\n\t\t_, apiErr := f.RestAPIClient.ClusterQueues.Resume(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, c.QueueUUID)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error resuming cluster queue: %w\", err)\n\t}\n\n\tvar queue buildkite.ClusterQueue\n\tif err = bkIO.SpinWhile(f, \"Loading cluster queue\", func() error {\n\t\tvar apiErr error\n\t\tqueue, _, apiErr = f.RestAPIClient.ClusterQueues.Get(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, c.QueueUUID)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error fetching cluster queue: %w\", err)\n\t}\n\n\tqueueView := output.Viewable[buildkite.ClusterQueue]{\n\t\tData:   queue,\n\t\tRender: renderQueueText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, queueView, format)\n\t}\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\tfmt.Fprintf(writer, \"Queue %s resumed successfully\\n\\n\", queue.Key)\n\treturn output.Write(writer, queueView, format)\n}\n"
  },
  {
    "path": "cmd/queue/update.go",
    "content": "package queue\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype UpdateCmd struct {\n\tClusterUUID        string `arg:\"\" help:\"Cluster UUID the queue belongs to\" name:\"cluster-uuid\"`\n\tQueueUUID          string `arg:\"\" help:\"Queue UUID to update\" name:\"queue-uuid\"`\n\tDescription        string `help:\"New description for the queue\" optional:\"\"`\n\tRetryAgentAffinity string `help:\"Retry agent affinity (prefer-warmest or prefer-different)\" optional:\"\" name:\"retry-agent-affinity\"`\n\toutput.OutputFlags\n}\n\nfunc (c *UpdateCmd) Validate() error {\n\tif c.Description == \"\" && c.RetryAgentAffinity == \"\" {\n\t\treturn fmt.Errorf(\"at least one of --description or --retry-agent-affinity must be provided\")\n\t}\n\n\tswitch buildkite.RetryAgentAffinity(c.RetryAgentAffinity) {\n\tcase \"\", buildkite.RetryAgentAffinityPreferWarmest, buildkite.RetryAgentAffinityPreferDifferent:\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\n\t\t\t\"invalid --retry-agent-affinity value %q: must be %s or %s\",\n\t\t\tc.RetryAgentAffinity,\n\t\t\tbuildkite.RetryAgentAffinityPreferWarmest,\n\t\t\tbuildkite.RetryAgentAffinityPreferDifferent,\n\t\t)\n\t}\n}\n\nfunc (c *UpdateCmd) Help() string {\n\treturn `\nAt least one of --description or --retry-agent-affinity must be provided.\n\nExamples:\n  # Update a queue's description\n  $ bk queue update my-cluster-uuid my-queue-uuid --description \"New description\"\n\n  # Update retry agent affinity\n  $ bk queue update my-cluster-uuid my-queue-uuid --retry-agent-affinity prefer-different\n\n  # Update both settings\n  $ bk queue update my-cluster-uuid my-queue-uuid --description \"New description\" --retry-agent-affinity prefer-warmest\n\n  # Output the updated queue as JSON\n  $ bk queue update my-cluster-uuid my-queue-uuid --description \"New description\" -o json\n`\n}\n\nfunc (c *UpdateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tinput := buildkite.ClusterQueueUpdate{\n\t\tDescription: c.Description,\n\t}\n\tif c.RetryAgentAffinity != \"\" {\n\t\tinput.RetryAgentAffinity = buildkite.RetryAgentAffinity(c.RetryAgentAffinity)\n\t}\n\n\tvar queue buildkite.ClusterQueue\n\tif err = bkIO.SpinWhile(f, \"Updating cluster queue\", func() error {\n\t\tvar apiErr error\n\t\tqueue, _, apiErr = f.RestAPIClient.ClusterQueues.Update(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, c.QueueUUID, input)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error updating cluster queue: %w\", err)\n\t}\n\n\tqueueView := output.Viewable[buildkite.ClusterQueue]{\n\t\tData:   queue,\n\t\tRender: renderQueueText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, queueView, format)\n\t}\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\tfmt.Fprintf(writer, \"Queue %s updated successfully\\n\\n\", queue.Key)\n\treturn output.Write(writer, queueView, format)\n}\n"
  },
  {
    "path": "cmd/queue/view.go",
    "content": "package queue\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype ViewCmd struct {\n\tClusterUUID string `arg:\"\" help:\"Cluster UUID the queue belongs to\" name:\"cluster-uuid\"`\n\tQueueUUID   string `arg:\"\" help:\"Queue UUID to view\" name:\"queue-uuid\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ViewCmd) Help() string {\n\treturn `\nExamples:\n  # View a queue\n  $ bk queue view my-cluster-uuid my-queue-uuid\n\n  # View a queue in JSON format\n  $ bk queue view my-cluster-uuid my-queue-uuid -o json\n`\n}\n\nfunc (c *ViewCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tvar queue buildkite.ClusterQueue\n\tif err = bkIO.SpinWhile(f, \"Loading cluster queue\", func() error {\n\t\tvar apiErr error\n\t\tqueue, _, apiErr = f.RestAPIClient.ClusterQueues.Get(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, c.QueueUUID)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error loading cluster queue: %w\", err)\n\t}\n\n\tqueueView := output.Viewable[buildkite.ClusterQueue]{\n\t\tData:   queue,\n\t\tRender: renderQueueText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, queueView, format)\n\t}\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\treturn output.Write(writer, queueView, format)\n}\n\nfunc renderQueueText(q buildkite.ClusterQueue) string {\n\tpaused := \"No\"\n\tif q.DispatchPaused {\n\t\tpaused = \"Yes\"\n\t}\n\n\trows := [][]string{\n\t\t{\"Key\", output.ValueOrDash(q.Key)},\n\t\t{\"Description\", output.ValueOrDash(q.Description)},\n\t\t{\"ID\", output.ValueOrDash(q.ID)},\n\t\t{\"GraphQL ID\", output.ValueOrDash(q.GraphQLID)},\n\t\t{\"Retry Agent Affinity\", output.ValueOrDash(string(q.RetryAgentAffinity))},\n\t\t{\"Dispatch Paused\", paused},\n\t\t{\"Web URL\", output.ValueOrDash(q.WebURL)},\n\t\t{\"API URL\", output.ValueOrDash(q.URL)},\n\t\t{\"Cluster URL\", output.ValueOrDash(q.ClusterURL)},\n\t}\n\n\tif q.DispatchPaused {\n\t\trows = append(rows, []string{\"Dispatch Paused Note\", output.ValueOrDash(q.DispatchPausedNote)})\n\t\tif q.DispatchPausedAt != nil {\n\t\t\trows = append(rows, []string{\"Dispatch Paused At\", q.DispatchPausedAt.Format(time.RFC3339)})\n\t\t}\n\t\tif q.DispatchPausedBy != nil {\n\t\t\trows = append(\n\t\t\t\trows,\n\t\t\t\t[]string{\"Dispatch Paused By Name\", output.ValueOrDash(q.DispatchPausedBy.Name)},\n\t\t\t\t[]string{\"Dispatch Paused By Email\", output.ValueOrDash(q.DispatchPausedBy.Email)},\n\t\t\t)\n\t\t}\n\t}\n\n\tif q.CreatedBy.ID != \"\" {\n\t\trows = append(\n\t\t\trows,\n\t\t\t[]string{\"Created By Name\", output.ValueOrDash(q.CreatedBy.Name)},\n\t\t\t[]string{\"Created By Email\", output.ValueOrDash(q.CreatedBy.Email)},\n\t\t\t[]string{\"Created By ID\", output.ValueOrDash(q.CreatedBy.ID)},\n\t\t)\n\t}\n\n\tif q.CreatedAt != nil {\n\t\trows = append(rows, []string{\"Created At\", q.CreatedAt.Format(time.RFC3339)})\n\t}\n\n\tvar sb strings.Builder\n\tfmt.Fprintf(&sb, \"Queue: %s\\n\\n\", output.ValueOrDash(q.Key))\n\n\ttable := output.Table(\n\t\t[]string{\"Field\", \"Value\"},\n\t\trows,\n\t\tmap[string]string{\"field\": \"dim\", \"value\": \"italic\"},\n\t)\n\n\tsb.WriteString(table)\n\treturn sb.String()\n}\n"
  },
  {
    "path": "cmd/secret/create.go",
    "content": "package secret\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype CreateCmd struct {\n\tClusterUUID string `help:\"The UUID of the cluster\" required:\"\" name:\"cluster-uuid\"`\n\tKey         string `help:\"The key name for the secret (e.g. MY_SECRET)\" required:\"\"`\n\tValue       string `help:\"The secret value. If not provided, you will be prompted to enter it.\" optional:\"\"`\n\tDescription string `help:\"A description of the secret\" optional:\"\"`\n\tPolicy      string `help:\"The access policy for the secret (YAML format)\" optional:\"\"`\n\toutput.OutputFlags\n}\n\nfunc (c *CreateCmd) Help() string {\n\treturn `\nCreate a new secret in a cluster.\n\nIf --value is not provided, you will be prompted to enter the secret value\ninteractively (input will be masked).\n\nExamples:\n  # Create a secret with interactive value input\n  $ bk secret create --cluster-uuid my-cluster-uuid --key MY_SECRET\n\n  # Create a secret with the value provided inline\n  $ bk secret create --cluster-uuid my-cluster-uuid --key MY_SECRET --value \"s3cr3t\"\n\n  # Create a secret with a description\n  $ bk secret create --cluster-uuid my-cluster-uuid --key MY_SECRET --description \"My secret description\"\n`\n}\n\nfunc (c *CreateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tvalue := c.Value\n\tif value == \"\" {\n\t\tif f.NoInput {\n\t\t\treturn fmt.Errorf(\"--value is required when --no-input is set\")\n\t\t}\n\t\tfmt.Fprint(os.Stderr, \"Enter secret value: \")\n\t\tvalue, err = bkIO.ReadPassword()\n\t\tfmt.Fprintln(os.Stderr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading secret value: %v\", err)\n\t\t}\n\t\tif value == \"\" {\n\t\t\treturn fmt.Errorf(\"secret value cannot be empty\")\n\t\t}\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tinput := buildkite.ClusterSecretCreate{\n\t\tKey:         c.Key,\n\t\tValue:       value,\n\t\tDescription: c.Description,\n\t\tPolicy:      c.Policy,\n\t}\n\n\tvar secret buildkite.ClusterSecret\n\tif err = bkIO.SpinWhile(f, \"Creating secret\", func() error {\n\t\tvar apiErr error\n\t\tsecret, _, apiErr = f.RestAPIClient.ClusterSecrets.Create(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, input)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error creating secret: %v\", err)\n\t}\n\n\tsecretView := output.Viewable[buildkite.ClusterSecret]{\n\t\tData:   secret,\n\t\tRender: renderSecretText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, secretView, format)\n\t}\n\n\tfmt.Fprintf(os.Stdout, \"Secret %s created successfully\\n\\n\", secret.Key)\n\treturn output.Write(os.Stdout, secretView, format)\n}\n"
  },
  {
    "path": "cmd/secret/delete.go",
    "content": "package secret\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n)\n\ntype DeleteCmd struct {\n\tClusterUUID string `help:\"The UUID of the cluster\" required:\"\" name:\"cluster-uuid\"`\n\tSecretID    string `help:\"The UUID of the secret to delete\" required:\"\" name:\"secret-id\"`\n}\n\nfunc (c *DeleteCmd) Help() string {\n\treturn `\nDelete a secret from a cluster.\n\nYou will be prompted to confirm deletion unless --yes is set.\n\nExamples:\n  # Delete a secret (with confirmation prompt)\n  $ bk secret delete --cluster-uuid my-cluster-uuid --secret-id my-secret-id\n\n  # Delete a secret without confirmation\n  $ bk secret delete --cluster-uuid my-cluster-uuid --secret-id my-secret-id --yes\n`\n}\n\nfunc (c *DeleteCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tconfirmed, err := bkIO.Confirm(f, fmt.Sprintf(\"Are you sure you want to delete secret %s?\", c.SecretID))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !confirmed {\n\t\tfmt.Fprintln(os.Stderr, \"Deletion cancelled.\")\n\t\treturn nil\n\t}\n\n\tif err = bkIO.SpinWhile(f, \"Deleting secret\", func() error {\n\t\t_, err = f.RestAPIClient.ClusterSecrets.Delete(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, c.SecretID)\n\t\treturn err\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error deleting secret: %v\", err)\n\t}\n\n\tfmt.Fprintln(os.Stderr, \"Secret deleted successfully.\")\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/secret/get.go",
    "content": "package secret\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype GetCmd struct {\n\tClusterUUID string `help:\"The UUID of the cluster\" required:\"\" name:\"cluster-uuid\"`\n\tSecretID    string `help:\"The UUID of the secret to view\" required:\"\" name:\"secret-id\"`\n\toutput.OutputFlags\n}\n\nfunc (c *GetCmd) Help() string {\n\treturn `\nView details of a cluster secret.\n\nExamples:\n  # View a secret\n  $ bk secret get --cluster-uuid my-cluster-uuid --secret-id my-secret-id\n\n  # View a secret in JSON format\n  $ bk secret get --cluster-uuid my-cluster-uuid --secret-id my-secret-id -o json\n`\n}\n\nfunc (c *GetCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tvar secret buildkite.ClusterSecret\n\tif err = bkIO.SpinWhile(f, \"Loading secret\", func() error {\n\t\tvar apiErr error\n\t\tsecret, _, apiErr = f.RestAPIClient.ClusterSecrets.Get(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, c.SecretID)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error fetching secret: %v\", err)\n\t}\n\n\tsecretView := output.Viewable[buildkite.ClusterSecret]{\n\t\tData:   secret,\n\t\tRender: renderSecretText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, secretView, format)\n\t}\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\treturn output.Write(writer, secretView, format)\n}\n\nfunc renderSecretText(s buildkite.ClusterSecret) string {\n\trows := [][]string{\n\t\t{\"Key\", output.ValueOrDash(s.Key)},\n\t\t{\"ID\", output.ValueOrDash(s.ID)},\n\t\t{\"Description\", output.ValueOrDash(s.Description)},\n\t\t{\"Policy\", output.ValueOrDash(s.Policy)},\n\t}\n\n\tif s.CreatedBy.ID != \"\" {\n\t\trows = append(\n\t\t\trows,\n\t\t\t[]string{\"Created By\", output.ValueOrDash(s.CreatedBy.Name)},\n\t\t)\n\t}\n\n\tif s.CreatedAt != nil {\n\t\trows = append(rows, []string{\"Created At\", s.CreatedAt.Format(time.RFC3339)})\n\t}\n\n\tif s.UpdatedBy != nil && s.UpdatedBy.ID != \"\" {\n\t\trows = append(\n\t\t\trows,\n\t\t\t[]string{\"Updated By\", output.ValueOrDash(s.UpdatedBy.Name)},\n\t\t)\n\t}\n\n\tif s.UpdatedAt != nil {\n\t\trows = append(rows, []string{\"Updated At\", s.UpdatedAt.Format(time.RFC3339)})\n\t}\n\n\tif s.LastReadAt != nil {\n\t\trows = append(rows, []string{\"Last Read At\", s.LastReadAt.Format(time.RFC3339)})\n\t}\n\n\tvar sb strings.Builder\n\tfmt.Fprintf(&sb, \"Viewing secret %s\\n\\n\", output.ValueOrDash(s.Key))\n\n\ttable := output.Table(\n\t\t[]string{\"Field\", \"Value\"},\n\t\trows,\n\t\tmap[string]string{\"field\": \"dim\", \"value\": \"italic\"},\n\t)\n\n\tsb.WriteString(table)\n\treturn sb.String()\n}\n"
  },
  {
    "path": "cmd/secret/list.go",
    "content": "package secret\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\tinternalSecret \"github.com/buildkite/cli/v3/internal/secret\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype ListCmd struct {\n\tClusterUUID string `help:\"The UUID of the cluster to list secrets for\" required:\"\" name:\"cluster-uuid\"`\n\toutput.OutputFlags\n}\n\nfunc (c *ListCmd) Help() string {\n\treturn `\nList secrets for a cluster.\n\nExamples:\n  # List all secrets in a cluster\n  $ bk secret list --cluster-uuid my-cluster-uuid\n\n  # List secrets in JSON format\n  $ bk secret list --cluster-uuid my-cluster-uuid -o json\n`\n}\n\nfunc (c *ListCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\tvar secrets []buildkite.ClusterSecret\n\tif err = bkIO.SpinWhile(f, \"Loading secrets\", func() error {\n\t\tvar apiErr error\n\t\tsecrets, _, apiErr = f.RestAPIClient.ClusterSecrets.List(ctx, f.Config.OrganizationSlug(), c.ClusterUUID, nil)\n\t\treturn apiErr\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error fetching secrets: %v\", err)\n\t}\n\n\tif len(secrets) == 0 {\n\t\treturn errors.New(\"no secrets found for cluster\")\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, secrets, format)\n\t}\n\n\tsummary := internalSecret.SecretViewTable(secrets...)\n\n\twriter, cleanup := bkIO.Pager(f.NoPager, f.Config.Pager())\n\tdefer func() { _ = cleanup() }()\n\n\tfmt.Fprintf(writer, \"%v\\n\", summary)\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/secret/secret_test.go",
    "content": "package secret\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestListSecrets(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"fetches secrets through API\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tsecrets := []buildkite.ClusterSecret{\n\t\t\t{\n\t\t\t\tID:          \"secret-1\",\n\t\t\t\tKey:         \"MY_SECRET\",\n\t\t\t\tDescription: \"A test secret\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tID:          \"secret-2\",\n\t\t\t\tKey:         \"ANOTHER_SECRET\",\n\t\t\t\tDescription: \"Another test secret\",\n\t\t\t},\n\t\t}\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != \"GET\" {\n\t\t\t\tt.Errorf(\"expected GET, got %s\", r.Method)\n\t\t\t}\n\t\t\tif !strings.Contains(r.URL.Path, \"/clusters/cluster-123/secrets\") {\n\t\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t\t}\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(secrets)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tresult, _, err := client.ClusterSecrets.List(context.Background(), \"test-org\", \"cluster-123\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(result) != 2 {\n\t\t\tt.Fatalf(\"expected 2 secrets, got %d\", len(result))\n\t\t}\n\n\t\tif result[0].Key != \"MY_SECRET\" {\n\t\t\tt.Errorf(\"expected key 'MY_SECRET', got %q\", result[0].Key)\n\t\t}\n\n\t\tif result[1].ID != \"secret-2\" {\n\t\t\tt.Errorf(\"expected ID 'secret-2', got %q\", result[1].ID)\n\t\t}\n\t})\n\n\tt.Run(\"empty result returns empty slice\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode([]buildkite.ClusterSecret{})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tresult, _, err := client.ClusterSecrets.List(context.Background(), \"test-org\", \"cluster-123\", nil)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif len(result) != 0 {\n\t\t\tt.Errorf(\"expected 0 secrets, got %d\", len(result))\n\t\t}\n\t})\n}\n\nfunc TestGetSecret(t *testing.T) {\n\tt.Parallel()\n\n\tsecret := buildkite.ClusterSecret{\n\t\tID:          \"secret-1\",\n\t\tKey:         \"MY_SECRET\",\n\t\tDescription: \"A test secret\",\n\t\tPolicy:      \"- pipeline_slug: my-pipeline\",\n\t}\n\n\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != \"GET\" {\n\t\t\tt.Errorf(\"expected GET, got %s\", r.Method)\n\t\t}\n\t\tif !strings.Contains(r.URL.Path, \"/secrets/secret-1\") {\n\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t}\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(w).Encode(secret)\n\t}))\n\tdefer s.Close()\n\n\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tresult, _, err := client.ClusterSecrets.Get(context.Background(), \"test-org\", \"cluster-123\", \"secret-1\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif result.Key != \"MY_SECRET\" {\n\t\tt.Errorf(\"expected key 'MY_SECRET', got %q\", result.Key)\n\t}\n\n\tif result.Policy != \"- pipeline_slug: my-pipeline\" {\n\t\tt.Errorf(\"expected policy, got %q\", result.Policy)\n\t}\n}\n\nfunc TestCreateSecret(t *testing.T) {\n\tt.Parallel()\n\n\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != \"POST\" {\n\t\t\tt.Errorf(\"expected POST, got %s\", r.Method)\n\t\t}\n\n\t\tvar input buildkite.ClusterSecretCreate\n\t\tif err := json.NewDecoder(r.Body).Decode(&input); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif input.Key != \"NEW_SECRET\" {\n\t\t\tt.Errorf(\"expected key 'NEW_SECRET', got %q\", input.Key)\n\t\t}\n\n\t\tif input.Value != \"s3cr3t\" {\n\t\t\tt.Errorf(\"expected value 's3cr3t', got %q\", input.Value)\n\t\t}\n\n\t\tif input.Description != \"A new secret\" {\n\t\t\tt.Errorf(\"expected description 'A new secret', got %q\", input.Description)\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tjson.NewEncoder(w).Encode(buildkite.ClusterSecret{\n\t\t\tID:          \"new-secret-id\",\n\t\t\tKey:         input.Key,\n\t\t\tDescription: input.Description,\n\t\t})\n\t}))\n\tdefer s.Close()\n\n\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tresult, _, err := client.ClusterSecrets.Create(context.Background(), \"test-org\", \"cluster-123\", buildkite.ClusterSecretCreate{\n\t\tKey:         \"NEW_SECRET\",\n\t\tValue:       \"s3cr3t\",\n\t\tDescription: \"A new secret\",\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif result.ID != \"new-secret-id\" {\n\t\tt.Errorf(\"expected ID 'new-secret-id', got %q\", result.ID)\n\t}\n\n\tif result.Key != \"NEW_SECRET\" {\n\t\tt.Errorf(\"expected key 'NEW_SECRET', got %q\", result.Key)\n\t}\n}\n\nfunc TestDeleteSecret(t *testing.T) {\n\tt.Parallel()\n\n\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != \"DELETE\" {\n\t\t\tt.Errorf(\"expected DELETE, got %s\", r.Method)\n\t\t}\n\t\tif !strings.Contains(r.URL.Path, \"/secrets/secret-to-delete\") {\n\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t}\n\t\tw.WriteHeader(http.StatusNoContent)\n\t}))\n\tdefer s.Close()\n\n\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t_, err = client.ClusterSecrets.Delete(context.Background(), \"test-org\", \"cluster-123\", \"secret-to-delete\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestUpdateSecret(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"updates metadata\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != \"PUT\" {\n\t\t\t\tt.Errorf(\"expected PUT, got %s\", r.Method)\n\t\t\t}\n\n\t\t\tvar input buildkite.ClusterSecretUpdate\n\t\t\tif err := json.NewDecoder(r.Body).Decode(&input); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif input.Description != \"Updated description\" {\n\t\t\t\tt.Errorf(\"expected description 'Updated description', got %q\", input.Description)\n\t\t\t}\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(buildkite.ClusterSecret{\n\t\t\t\tID:          \"secret-1\",\n\t\t\t\tKey:         \"MY_SECRET\",\n\t\t\t\tDescription: input.Description,\n\t\t\t})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tresult, _, err := client.ClusterSecrets.Update(context.Background(), \"test-org\", \"cluster-123\", \"secret-1\", buildkite.ClusterSecretUpdate{\n\t\t\tDescription: \"Updated description\",\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif result.Description != \"Updated description\" {\n\t\t\tt.Errorf(\"expected description 'Updated description', got %q\", result.Description)\n\t\t}\n\t})\n\n\tt.Run(\"updates value\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.Method != \"PUT\" {\n\t\t\t\tt.Errorf(\"expected PUT, got %s\", r.Method)\n\t\t\t}\n\t\t\tif !strings.Contains(r.URL.Path, \"/secrets/secret-1/value\") {\n\t\t\t\tt.Errorf(\"unexpected path: %s\", r.URL.Path)\n\t\t\t}\n\n\t\t\tvar input buildkite.ClusterSecretValueUpdate\n\t\t\tif err := json.NewDecoder(r.Body).Decode(&input); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tif input.Value != \"new-value\" {\n\t\t\t\tt.Errorf(\"expected value 'new-value', got %q\", input.Value)\n\t\t\t}\n\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(s.URL))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t_, err = client.ClusterSecrets.UpdateValue(context.Background(), \"test-org\", \"cluster-123\", \"secret-1\", buildkite.ClusterSecretValueUpdate{\n\t\t\tValue: \"new-value\",\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t})\n}\n\nfunc TestUpdateCmdValidate(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname    string\n\t\tcmd     UpdateCmd\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"no flags set\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"c\", SecretID: \"s\"},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"only description\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"c\", SecretID: \"s\", Description: \"new desc\"},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"only policy\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"c\", SecretID: \"s\", Policy: \"new policy\"},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"only update-value\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"c\", SecretID: \"s\", UpdateValue: true},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"description and update-value\",\n\t\t\tcmd:     UpdateCmd{ClusterUUID: \"c\", SecretID: \"s\", Description: \"desc\", UpdateValue: true},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\terr := tt.cmd.Validate()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRenderSecretText(t *testing.T) {\n\tt.Parallel()\n\n\tsecret := buildkite.ClusterSecret{\n\t\tID:          \"secret-123\",\n\t\tKey:         \"MY_SECRET\",\n\t\tDescription: \"Test description\",\n\t\tPolicy:      \"- pipeline_slug: test\",\n\t\tCreatedBy: buildkite.SecretCreator{\n\t\t\tID:   \"user-1\",\n\t\t\tName: \"Test User\",\n\t\t},\n\t}\n\n\tresult := renderSecretText(secret)\n\n\texpectedStrings := []string{\n\t\t\"Viewing secret MY_SECRET\",\n\t\t\"secret-123\",\n\t\t\"Test description\",\n\t\t\"- pipeline_slug: test\",\n\t\t\"Test User\",\n\t}\n\n\tfor _, expected := range expectedStrings {\n\t\tif !strings.Contains(result, expected) {\n\t\t\tt.Errorf(\"expected output to contain %q, got:\\n%s\", expected, result)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cmd/secret/update.go",
    "content": "package secret\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype UpdateCmd struct {\n\tClusterUUID string `help:\"The UUID of the cluster\" required:\"\" name:\"cluster-uuid\"`\n\tSecretID    string `help:\"The UUID of the secret to update\" required:\"\" name:\"secret-id\"`\n\tDescription string `help:\"Update the description of the secret\" optional:\"\"`\n\tPolicy      string `help:\"Update the access policy for the secret (YAML format)\" optional:\"\"`\n\tUpdateValue bool   `help:\"Prompt to update the secret value\" optional:\"\" name:\"update-value\"`\n\toutput.OutputFlags\n}\n\nfunc (c *UpdateCmd) Help() string {\n\treturn `\nUpdate a cluster secret's description, policy, or value.\n\nUse --update-value to be prompted for a new secret value (input will be masked).\n\nExamples:\n  # Update a secret's description\n  $ bk secret update --cluster-uuid my-cluster-uuid --secret-id my-secret-id --description \"New description\"\n\n  # Update a secret's value\n  $ bk secret update --cluster-uuid my-cluster-uuid --secret-id my-secret-id --update-value\n\n  # Update both description and value\n  $ bk secret update --cluster-uuid my-cluster-uuid --secret-id my-secret-id --description \"New description\" --update-value\n`\n}\n\nfunc (c *UpdateCmd) Validate() error {\n\tif c.Description == \"\" && c.Policy == \"\" && !c.UpdateValue {\n\t\treturn fmt.Errorf(\"at least one of --description, --policy, or --update-value must be provided\")\n\t}\n\treturn nil\n}\n\nfunc (c *UpdateCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\tf.NoPager = f.NoPager || globals.DisablePager()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\torg := f.Config.OrganizationSlug()\n\n\tif c.UpdateValue {\n\t\tif f.NoInput {\n\t\t\treturn fmt.Errorf(\"--update-value requires interactive input but --no-input is set\")\n\t\t}\n\t\tfmt.Fprint(os.Stderr, \"Enter new secret value: \")\n\t\tvalue, err := bkIO.ReadPassword()\n\t\tfmt.Fprintln(os.Stderr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading secret value: %v\", err)\n\t\t}\n\t\tif value == \"\" {\n\t\t\treturn fmt.Errorf(\"secret value cannot be empty\")\n\t\t}\n\n\t\tif err = bkIO.SpinWhile(f, \"Updating secret value\", func() error {\n\t\t\t_, err = f.RestAPIClient.ClusterSecrets.UpdateValue(ctx, org, c.ClusterUUID, c.SecretID, buildkite.ClusterSecretValueUpdate{\n\t\t\t\tValue: value,\n\t\t\t})\n\t\t\treturn err\n\t\t}); err != nil {\n\t\t\treturn fmt.Errorf(\"error updating secret value: %v\", err)\n\t\t}\n\t}\n\n\tvar secret buildkite.ClusterSecret\n\tif c.Description != \"\" || c.Policy != \"\" {\n\t\tif err = bkIO.SpinWhile(f, \"Updating secret\", func() error {\n\t\t\tvar apiErr error\n\t\t\tsecret, _, apiErr = f.RestAPIClient.ClusterSecrets.Update(ctx, org, c.ClusterUUID, c.SecretID, buildkite.ClusterSecretUpdate{\n\t\t\t\tDescription: c.Description,\n\t\t\t\tPolicy:      c.Policy,\n\t\t\t})\n\t\t\treturn apiErr\n\t\t}); err != nil {\n\t\t\treturn fmt.Errorf(\"error updating secret: %v\", err)\n\t\t}\n\t} else {\n\t\t// Fetch the secret to display current state\n\t\tif err = bkIO.SpinWhile(f, \"Loading secret\", func() error {\n\t\t\tvar apiErr error\n\t\t\tsecret, _, apiErr = f.RestAPIClient.ClusterSecrets.Get(ctx, org, c.ClusterUUID, c.SecretID)\n\t\t\treturn apiErr\n\t\t}); err != nil {\n\t\t\treturn fmt.Errorf(\"error fetching secret: %v\", err)\n\t\t}\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tsecretView := output.Viewable[buildkite.ClusterSecret]{\n\t\tData:   secret,\n\t\tRender: renderSecretText,\n\t}\n\n\tif format != output.FormatText {\n\t\treturn output.Write(os.Stdout, secretView, format)\n\t}\n\n\tfmt.Fprintln(os.Stderr, \"Secret updated successfully.\")\n\tfmt.Fprintln(os.Stdout)\n\treturn output.Write(os.Stdout, secretView, format)\n}\n"
  },
  {
    "path": "cmd/skill/skill.go",
    "content": "package skill\n\nimport (\n\t\"archive/zip\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst (\n\tdefaultRepo   = \"buildkite/skills\"\n\tdefaultBranch = \"main\"\n)\n\ntype AddCmd struct {\n\tName   string `arg:\"\" help:\"Name of the Buildkite skill to install (for example, buildkite-api).\"`\n\tAgent  string `help:\"Agent/harness to install for (claude or cursor). Auto-detected from .claude or .cursor by default.\" optional:\"\"`\n\tGlobal bool   `help:\"Install globally in your home directory instead of the current project.\"`\n\tPath   string `help:\"Custom skills directory to install into, for agents such as Amp or Pi.\" type:\"path\" optional:\"\" aliases:\"to,location\"`\n\tForce  bool   `help:\"Overwrite an existing installed skill.\"`\n\tRepo   string `help:\"GitHub repository to install skills from.\" default:\"${skill_repo}\" hidden:\"\"`\n\tBranch string `help:\"Git branch to install skills from.\" default:\"${skill_branch}\" hidden:\"\"`\n}\n\nfunc (c *AddCmd) Help() string {\n\treturn `Install a Buildkite skill from github.com/buildkite/skills.\n\nBy default, the target is auto-detected from a project .claude or .cursor\nfolder. Use --agent to choose a target explicitly, --global to install to all\nexisting global agent directories (~/.claude and/or ~/.cursor), or --path for\nanother agent's skills directory.\n\nExamples:\n  # Install buildkite-api into the detected project agent\n  $ bk skill add buildkite-api\n\n  # Install for Claude Code in this project\n  $ bk skill add buildkite-api --agent claude\n\n  # Install globally for Cursor\n  $ bk skill add buildkite-api --agent cursor --global\n\n  # Install into a custom skills directory, such as Amp or Pi\n  $ bk skill add buildkite-api --path ~/.amp/skills\n`\n}\n\nfunc (c *AddCmd) Run() error {\n\tif err := validateSkillName(c.Name); err != nil {\n\t\treturn err\n\t}\n\treturn installSkill(c.Name, c.Agent, c.Global, c.Path, c.Force, c.Repo, c.Branch)\n}\n\ntype UpdateCmd struct {\n\tName   string `arg:\"\" help:\"Name of the installed Buildkite skill to update. If omitted, all installed skills are updated.\" optional:\"\"`\n\tAgent  string `help:\"Agent/harness to update for (claude or cursor). Auto-detected from .claude or .cursor by default.\" optional:\"\"`\n\tGlobal bool   `help:\"Update the globally installed skill instead of the current project.\"`\n\tPath   string `help:\"Custom skills directory to update, for agents such as Amp or Pi.\" type:\"path\" optional:\"\"`\n\tRepo   string `help:\"GitHub repository to install skills from.\" default:\"${skill_repo}\" hidden:\"\"`\n\tBranch string `help:\"Git branch to install skills from.\" default:\"${skill_branch}\" hidden:\"\"`\n}\n\nfunc (c *UpdateCmd) Help() string {\n\treturn `Update installed Buildkite skills from github.com/buildkite/skills.\n\nIf no skill name is provided, all currently installed skills for the target agent\nare updated.\n\nExamples:\n  $ bk skill update\n  $ bk skill update buildkite-api\n  $ bk skill update buildkite-api --agent claude --global\n  $ bk skill update --path ~/.amp/skills\n`\n}\n\nfunc (c *UpdateCmd) Run() error {\n\tif c.Name != \"\" {\n\t\tif err := validateSkillName(c.Name); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\ttargets, err := resolveTargets(c.Agent, c.Global, c.Path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.Name != \"\" {\n\t\tvar installedTargets []target\n\t\tfor _, target := range targets {\n\t\t\tif info, err := os.Stat(filepath.Join(target.SkillsDir(), c.Name)); err == nil && info.IsDir() {\n\t\t\t\tinstalledTargets = append(installedTargets, target)\n\t\t\t} else if err != nil && !os.IsNotExist(err) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif len(installedTargets) == 0 {\n\t\t\treturn fmt.Errorf(\"skill %q is not installed for any selected target\", c.Name)\n\t\t}\n\t\treturn installSkillToTargets(c.Name, installedTargets, true, c.Repo, c.Branch)\n\t}\n\n\tplan := map[string][]target{}\n\tfor _, target := range targets {\n\t\tentries, err := os.ReadDir(target.SkillsDir())\n\t\tif err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tfor _, entry := range entries {\n\t\t\tif entry.IsDir() {\n\t\t\t\tplan[entry.Name()] = append(plan[entry.Name()], target)\n\t\t\t}\n\t\t}\n\t}\n\tif len(plan) == 0 {\n\t\treturn fmt.Errorf(\"no skills are installed for any selected target\")\n\t}\n\n\treturn installSkillsToTargets(plan, true, c.Repo, c.Branch)\n}\n\ntype DeleteCmd struct {\n\tName   string `arg:\"\" help:\"Name of the installed Buildkite skill to delete.\"`\n\tAgent  string `help:\"Agent/harness to delete from (claude or cursor). Auto-detected from .claude or .cursor by default.\" optional:\"\"`\n\tGlobal bool   `help:\"Delete the globally installed skill instead of the current project.\"`\n\tPath   string `help:\"Custom skills directory to delete from, for agents such as Amp or Pi.\" type:\"path\" optional:\"\"`\n}\n\nfunc (c *DeleteCmd) Help() string {\n\treturn `Delete an installed Buildkite skill.\n\nExamples:\n  $ bk skill delete buildkite-api\n  $ bk skill delete buildkite-api --agent cursor --global\n  $ bk skill delete buildkite-api --path ~/.amp/skills\n`\n}\n\nfunc (c *DeleteCmd) Run() error {\n\tif err := validateSkillName(c.Name); err != nil {\n\t\treturn err\n\t}\n\ttargets, err := resolveTargets(c.Agent, c.Global, c.Path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar installedTargets []target\n\tfor _, target := range targets {\n\t\tdest := filepath.Join(target.SkillsDir(), c.Name)\n\t\tif info, err := os.Stat(dest); err == nil && info.IsDir() {\n\t\t\tinstalledTargets = append(installedTargets, target)\n\t\t} else if err != nil && !os.IsNotExist(err) {\n\t\t\treturn err\n\t\t}\n\t}\n\tif len(installedTargets) == 0 {\n\t\treturn fmt.Errorf(\"skill %q is not installed for any selected target\", c.Name)\n\t}\n\n\tfor _, target := range installedTargets {\n\t\tdest := filepath.Join(target.SkillsDir(), c.Name)\n\t\tif err := os.RemoveAll(dest); err != nil {\n\t\t\treturn fmt.Errorf(\"deleting skill %q: %w\", c.Name, err)\n\t\t}\n\t\tfmt.Printf(\"Deleted %s skill %q from %s\\n\", target.agent, c.Name, dest)\n\t}\n\treturn nil\n}\n\ntype target struct {\n\tagent     string\n\troot      string\n\tskillsDir string\n}\n\nfunc (t target) SkillsDir() string {\n\tif t.skillsDir != \"\" {\n\t\treturn t.skillsDir\n\t}\n\treturn filepath.Join(t.root, \"skills\")\n}\n\nfunc resolveTarget(agent string, global bool, customPath string) (target, error) {\n\ttargets, err := resolveTargets(agent, global, customPath)\n\tif err != nil {\n\t\treturn target{}, err\n\t}\n\treturn targets[0], nil\n}\n\nfunc resolveTargets(agent string, global bool, customPath string) ([]target, error) {\n\tif global && customPath != \"\" {\n\t\treturn nil, fmt.Errorf(\"--global and --path cannot be used together\")\n\t}\n\tif customPath == \"\" && agent != \"\" && agent != \"claude\" && agent != \"cursor\" {\n\t\treturn nil, fmt.Errorf(\"unsupported --agent %q (expected claude or cursor, or use --path for a custom agent)\", agent)\n\t}\n\n\tif customPath != \"\" {\n\t\tabs, err := filepath.Abs(customPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif agent == \"\" {\n\t\t\tagent = \"custom\"\n\t\t}\n\t\treturn []target{{agent: agent, skillsDir: abs}}, nil\n\t}\n\n\tif global {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif agent != \"\" {\n\t\t\troot := filepath.Join(home, \".\"+agent)\n\t\t\tif !dirExists(root) {\n\t\t\t\treturn nil, fmt.Errorf(\"global %s directory does not exist at %s\", agent, root)\n\t\t\t}\n\t\t\treturn []target{{agent: agent, root: root}}, nil\n\t\t}\n\n\t\tvar targets []target\n\t\tfor _, candidate := range []string{\"claude\", \"cursor\"} {\n\t\t\troot := filepath.Join(home, \".\"+candidate)\n\t\t\tif dirExists(root) {\n\t\t\t\ttargets = append(targets, target{agent: candidate, root: root})\n\t\t\t}\n\t\t}\n\t\tif len(targets) == 0 {\n\t\t\treturn nil, fmt.Errorf(\"no global agent directories found at ~/.claude or ~/.cursor\")\n\t\t}\n\t\treturn targets, nil\n\t}\n\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif agent == \"\" {\n\t\tif dirExists(filepath.Join(wd, \".claude\")) {\n\t\t\tagent = \"claude\"\n\t\t} else if dirExists(filepath.Join(wd, \".cursor\")) {\n\t\t\tagent = \"cursor\"\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"could not detect an agent target: create .claude or .cursor, or pass --agent claude|cursor\")\n\t\t}\n\t}\n\treturn []target{{agent: agent, root: filepath.Join(wd, \".\"+agent)}}, nil\n}\n\nfunc dirExists(path string) bool {\n\tinfo, err := os.Stat(path)\n\treturn err == nil && info.IsDir()\n}\n\nfunc installSkill(name, agent string, global bool, customPath string, force bool, repo, branch string) error {\n\ttargets, err := resolveTargets(agent, global, customPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn installSkillToTargets(name, targets, force, repo, branch)\n}\n\nfunc installSkillToTargets(name string, targets []target, force bool, repo, branch string) error {\n\treturn installSkillsToTargets(map[string][]target{name: targets}, force, repo, branch)\n}\n\nfunc installSkillsToTargets(plan map[string][]target, force bool, repo, branch string) error {\n\tfor name, targets := range plan {\n\t\tif err := validateSkillName(name); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, target := range targets {\n\t\t\tdest := filepath.Join(target.SkillsDir(), name)\n\t\t\tif !force {\n\t\t\t\tif _, err := os.Stat(dest); err == nil {\n\t\t\t\t\treturn fmt.Errorf(\"skill %q is already installed at %s (use --force or bk skill update)\", name, dest)\n\t\t\t\t} else if !os.IsNotExist(err) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\ttmpDir, err := os.MkdirTemp(\"\", \"bk-skill-*\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.RemoveAll(tmpDir)\n\n\tarchive := filepath.Join(tmpDir, \"skills.zip\")\n\tif err := downloadRepoArchive(repo, branch, archive); err != nil {\n\t\treturn err\n\t}\n\n\tcounter := 0\n\tfor name, targets := range plan {\n\t\tfor _, target := range targets {\n\t\t\tdest := filepath.Join(target.SkillsDir(), name)\n\t\t\textracted := filepath.Join(tmpDir, fmt.Sprintf(\"%s-%d\", name, counter))\n\t\t\tcounter++\n\t\t\tif err := extractSkill(archive, name, extracted); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := os.MkdirAll(target.SkillsDir(), 0o755); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := os.RemoveAll(dest); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := os.Rename(extracted, dest); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Printf(\"Installed %s skill %q to %s\\n\", target.agent, name, dest)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validateSkillName(name string) error {\n\tif name == \"\" || name == \".\" || name == \"..\" {\n\t\treturn fmt.Errorf(\"invalid skill name %q\", name)\n\t}\n\tfor _, r := range name {\n\t\tif (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' || r == '.' {\n\t\t\tcontinue\n\t\t}\n\t\treturn fmt.Errorf(\"invalid skill name %q: use a literal skill name, not a path, URL, or pattern\", name)\n\t}\n\treturn nil\n}\n\nfunc downloadRepoArchive(repo, branch, dest string) error {\n\turl := fmt.Sprintf(\"https://codeload.github.com/%s/zip/refs/heads/%s\", repo, branch)\n\tclient := &http.Client{Timeout: 60 * time.Second}\n\tresp, err := client.Get(url)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"downloading %s: %w\", url, err)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"downloading %s: unexpected HTTP status %s\", url, resp.Status)\n\t}\n\tout, err := os.Create(dest)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer out.Close()\n\t_, err = io.Copy(out, resp.Body)\n\treturn err\n}\n\nfunc extractSkill(archive, skillName, dest string) error {\n\tr, err := zip.OpenReader(archive)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"opening downloaded skills archive: %w\", err)\n\t}\n\tdefer r.Close()\n\n\tfound := false\n\tfor _, f := range r.File {\n\t\tparts := strings.SplitN(f.Name, \"/\", 4)\n\t\tvar rel string\n\t\tswitch {\n\t\tcase len(parts) >= 3 && parts[1] == skillName:\n\t\t\trel = parts[2]\n\t\tcase len(parts) >= 4 && parts[1] == \"skills\" && parts[2] == skillName:\n\t\t\trel = parts[3]\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t\tif rel == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tfound = true\n\t\tpath := filepath.Join(dest, filepath.FromSlash(rel))\n\t\tif !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {\n\t\t\treturn fmt.Errorf(\"archive contains invalid path %q\", f.Name)\n\t\t}\n\t\tif f.FileInfo().IsDir() {\n\t\t\tif err := os.MkdirAll(path, 0o755); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {\n\t\t\treturn err\n\t\t}\n\t\trc, err := f.Open()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tout, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())\n\t\tif err != nil {\n\t\t\trc.Close()\n\t\t\treturn err\n\t\t}\n\t\t_, copyErr := io.Copy(out, rc)\n\t\tcloseErr := out.Close()\n\t\trc.Close()\n\t\tif copyErr != nil {\n\t\t\treturn copyErr\n\t\t}\n\t\tif closeErr != nil {\n\t\t\treturn closeErr\n\t\t}\n\t}\n\tif !found {\n\t\treturn fmt.Errorf(\"skill %q not found in github.com/%s\", skillName, defaultRepo)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/skill/skill_test.go",
    "content": "package skill\n\nimport (\n\t\"archive/zip\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestResolveTargetDetectsProjectAgent(t *testing.T) {\n\tdir := t.TempDir()\n\toldWD, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer os.Chdir(oldWD)\n\tif err := os.Chdir(dir); err != nil {\n\t\tt.Fatal(err)\n\t}\n\twd, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Mkdir(\".cursor\", 0o755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttarget, err := resolveTarget(\"\", false, \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif target.agent != \"cursor\" {\n\t\tt.Fatalf(\"agent = %q, want cursor\", target.agent)\n\t}\n\tif want := filepath.Join(wd, \".cursor\", \"skills\"); target.SkillsDir() != want {\n\t\tt.Fatalf(\"skills dir = %q, want %q\", target.SkillsDir(), want)\n\t}\n}\n\nfunc TestResolveTargetErrorsWithoutProjectAgent(t *testing.T) {\n\tdir := t.TempDir()\n\toldWD, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer os.Chdir(oldWD)\n\tif err := os.Chdir(dir); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif _, err := resolveTarget(\"\", false, \"\"); err == nil {\n\t\tt.Fatal(\"expected error\")\n\t}\n}\n\nfunc TestResolveTargetUsesCustomPath(t *testing.T) {\n\tdir := filepath.Join(t.TempDir(), \"amp-skills\")\n\ttarget, err := resolveTarget(\"\", false, dir)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif target.agent != \"custom\" {\n\t\tt.Fatalf(\"agent = %q, want custom\", target.agent)\n\t}\n\twant, err := filepath.Abs(dir)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif target.SkillsDir() != want {\n\t\tt.Fatalf(\"skills dir = %q, want %q\", target.SkillsDir(), want)\n\t}\n}\n\nfunc TestResolveTargetsGlobalUsesAllExistingAgentDirs(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\tif err := os.Mkdir(filepath.Join(home, \".claude\"), 0o755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Mkdir(filepath.Join(home, \".cursor\"), 0o755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttargets, err := resolveTargets(\"\", true, \"\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(targets) != 2 {\n\t\tt.Fatalf(\"got %d targets, want 2\", len(targets))\n\t}\n\tif targets[0].agent != \"claude\" || targets[1].agent != \"cursor\" {\n\t\tt.Fatalf(\"targets = %#v, want claude then cursor\", targets)\n\t}\n}\n\nfunc TestResolveTargetsGlobalDoesNotCreateAgentDirs(t *testing.T) {\n\thome := t.TempDir()\n\tt.Setenv(\"HOME\", home)\n\n\tif _, err := resolveTargets(\"\", true, \"\"); err == nil {\n\t\tt.Fatal(\"expected error\")\n\t}\n\tif dirExists(filepath.Join(home, \".claude\")) || dirExists(filepath.Join(home, \".cursor\")) {\n\t\tt.Fatal(\"global detection created agent directories\")\n\t}\n}\n\nfunc TestValidateSkillNameRejectsPathsURLsAndPatterns(t *testing.T) {\n\tfor _, name := range []string{\"../skill\", \"skills/buildkite-api\", \"https://example.com/skill\", \"buildkite-*\", \"\"} {\n\t\tif err := validateSkillName(name); err == nil {\n\t\t\tt.Fatalf(\"validateSkillName(%q) succeeded, want error\", name)\n\t\t}\n\t}\n}\n\nfunc TestDeleteErrorsWhenSkillIsNotInstalled(t *testing.T) {\n\tdir := t.TempDir()\n\tcmd := DeleteCmd{Name: \"missing\", Path: dir}\n\tif err := cmd.Run(); err == nil {\n\t\tt.Fatal(\"expected error\")\n\t}\n}\n\nfunc TestExtractSkill(t *testing.T) {\n\tarchive := filepath.Join(t.TempDir(), \"skills.zip\")\n\tcreateZip(t, archive, map[string]string{\n\t\t\"skills-main/skills/buildkite-api/SKILL.md\":    \"# Buildkite API\",\n\t\t\"skills-main/skills/buildkite-api/docs/ref.md\": \"reference\",\n\t\t\"skills-main/skills/other/SKILL.md\":            \"# Other\",\n\t})\n\n\tdest := filepath.Join(t.TempDir(), \"buildkite-api\")\n\tif err := extractSkill(archive, \"buildkite-api\", dest); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tgot, err := os.ReadFile(filepath.Join(dest, \"SKILL.md\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif string(got) != \"# Buildkite API\" {\n\t\tt.Fatalf(\"SKILL.md = %q\", got)\n\t}\n\tif _, err := os.Stat(filepath.Join(dest, \"docs\", \"ref.md\")); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := os.Stat(filepath.Join(dest, \"..\", \"other\")); err == nil {\n\t\tt.Fatal(\"extracted another skill\")\n\t}\n}\n\nfunc createZip(t *testing.T, path string, files map[string]string) {\n\tt.Helper()\n\tout, err := os.Create(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer out.Close()\n\n\tzw := zip.NewWriter(out)\n\tfor name, content := range files {\n\t\tw, err := zw.Create(name)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif _, err := w.Write([]byte(content)); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\tif err := zw.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "cmd/use/use.go",
    "content": "package use\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\ntype UseCmd struct {\n\tOrganizationSlug string `arg:\"\" optional:\"\" help:\"Organization slug to use\"`\n}\n\nfunc (c *UseCmd) Help() string {\n\treturn `Select a configured organization.\n\nExamples:\n\t# Use the 'my-cool-org' configuration\n\t$ bk use my-cool-org\n\n\t# Interactively select an organization\n\t$ bk use\n`\n}\n\nfunc (c *UseCmd) Run(globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.NoInput = globals.DisableInput()\n\n\tvar org *string\n\tif c.OrganizationSlug != \"\" {\n\t\torg = &c.OrganizationSlug\n\t}\n\n\treturn useRun(org, f.Config, f.GitRepository != nil, f.NoInput)\n}\n\nfunc useRun(org *string, conf *config.Config, inGitRepo bool, noInput bool) error {\n\tvar selected string\n\n\t// prompt to choose from configured orgs if one is not already selected\n\tif org == nil {\n\t\tvar err error\n\t\tselected, err = io.PromptForOne(\"organization\", conf.ConfiguredOrganizations(), noInput)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tselected = *org\n\t}\n\n\t// if already selected, do nothing\n\tif conf.OrganizationSlug() == selected {\n\t\tfmt.Printf(\"Using configuration for `%s`\\n\", selected)\n\t\treturn nil\n\t}\n\n\t// if the selected org exists, use it\n\tif conf.HasConfiguredOrganization(selected) {\n\t\tfmt.Printf(\"Using configuration for `%s`\\n\", selected)\n\t\treturn conf.SelectOrganization(selected, inGitRepo)\n\t}\n\n\t// If token exists in keychain or config but org marker is missing (selected_org in .bk.yaml), register it\n\t// so org switching/listing works going forward.\n\tif conf.HasStoredTokenForOrg(selected) {\n\t\tif err := conf.EnsureOrganization(selected); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to register configuration for `%s`: %w\", selected, err)\n\t\t}\n\t\tfmt.Printf(\"Using configuration for `%s`\\n\", selected)\n\t\treturn conf.SelectOrganization(selected, inGitRepo)\n\t}\n\n\t// if the selected org doesnt exist, recommend configuring it and error out\n\treturn fmt.Errorf(\"no configuration found for `%s`. run `bk configure` to add it\", selected)\n}\n"
  },
  {
    "path": "cmd/user/invite.go",
    "content": "package user\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/internal/graphql\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n)\n\ntype InviteCmd struct {\n\tEmails []string `arg:\"\" required:\"\" help:\"Email addresses to invite\"`\n}\n\nfunc (c *InviteCmd) Help() string {\n\treturn `\nExamples:\n  # Invite a single user to your organization\n  $ bk user invite bob@supercoolorg.com\n\n  # Invite multiple users to your organization\n  $ bk user invite bob@supercoolorg.com bobs_mate@supercoolorg.com\n`\n}\n\nfunc (c *InviteCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf.SkipConfirm = globals.SkipConfirmation()\n\tf.NoInput = globals.DisableInput()\n\tf.Quiet = globals.IsQuiet()\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tctx := context.Background()\n\n\torgID, err := graphql.GetOrganizationID(ctx, f.GraphQLClient, f.Config.OrganizationSlug())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn createInvite(ctx, f, orgID.Organization.GetId(), c.Emails...)\n}\n\nfunc createInvite(ctx context.Context, f *factory.Factory, orgID string, emails ...string) error {\n\tif len(emails) == 0 {\n\t\treturn nil\n\t}\n\n\terrChan := make(chan error, len(emails))\n\tvar wg sync.WaitGroup\n\n\tfor _, email := range emails {\n\t\twg.Add(1)\n\t\tgo func(email string) {\n\t\t\tdefer wg.Done()\n\t\t\t_, err := graphql.InviteUser(ctx, f.GraphQLClient, orgID, []string{email})\n\t\t\tif err != nil {\n\t\t\t\terrChan <- fmt.Errorf(\"error creating user invite for %s: %w\", email, err)\n\t\t\t}\n\t\t}(email)\n\t}\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(errChan)\n\t}()\n\n\tvar errs []error\n\tfor err := range errChan {\n\t\terrs = append(errs, err)\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn fmt.Errorf(\"errors creating user invites: %v\", errs)\n\t}\n\n\tmessage := \"Invite sent to\"\n\tif len(emails) > 1 {\n\t\tmessage = \"Invites sent to\"\n\t}\n\n\tfmt.Printf(\"%s: %v\\n\", message, emails)\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/version/update_check.go",
    "content": "package version\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\nvar releaseURL = \"https://api.github.com/repos/buildkite/cli/releases/latest\"\n\ntype githubRelease struct {\n\tTagName string `json:\"tag_name\"`\n}\n\n// CheckForUpdate checks GitHub for the latest release and returns the latest\n// version string and whether it is newer than currentVersion.\n// Returns (\"\", false) silently on any error or if no update is available.\nfunc CheckForUpdate(currentVersion string) (string, bool) {\n\tcurrent := strings.TrimPrefix(currentVersion, \"v\")\n\tif parseVersion(current) == nil {\n\t\treturn \"\", false\n\t}\n\n\tclient := &http.Client{Timeout: 3 * time.Second}\n\tresp, err := client.Get(releaseURL)\n\tif err != nil {\n\t\treturn \"\", false\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", false\n\t}\n\n\tvar release githubRelease\n\tif err := json.NewDecoder(resp.Body).Decode(&release); err != nil {\n\t\treturn \"\", false\n\t}\n\n\tlatest := strings.TrimPrefix(release.TagName, \"v\")\n\n\tif isNewer(latest, current) {\n\t\treturn latest, true\n\t}\n\n\treturn \"\", false\n}\n\n// isNewer returns true if version a is strictly newer than version b.\n// Both versions are expected to be in \"major.minor.patch\" format.\nfunc isNewer(a, b string) bool {\n\taParts := parseVersion(a)\n\tbParts := parseVersion(b)\n\tif aParts == nil || bParts == nil {\n\t\treturn false\n\t}\n\n\tfor i := range 3 {\n\t\tif aParts[i] > bParts[i] {\n\t\t\treturn true\n\t\t}\n\t\tif aParts[i] < bParts[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn false\n}\n\n// parseVersion splits a \"major.minor.patch\" string into three integers.\n// Returns nil if the format is invalid.\nfunc parseVersion(v string) []int {\n\tparts := strings.SplitN(v, \".\", 3)\n\tif len(parts) != 3 {\n\t\treturn nil\n\t}\n\n\tnums := make([]int, 3)\n\tfor i, p := range parts {\n\t\tn, err := strconv.Atoi(p)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tnums[i] = n\n\t}\n\treturn nums\n}\n\n// FormatUpdateNudge returns the nudge message for display.\nfunc FormatUpdateNudge(latestVersion string) string {\n\treturn fmt.Sprintf(\"A new version of bk is available: %s\\n\", latestVersion)\n}\n"
  },
  {
    "path": "cmd/version/update_check_test.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestCheckForUpdate_NewerVersionAvailable(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprint(w, `{\"tag_name\": \"v2.0.0\"}`)\n\t}))\n\tdefer server.Close()\n\n\treleaseURL = server.URL\n\n\tlatest, hasUpdate := CheckForUpdate(\"1.0.0\")\n\tif !hasUpdate {\n\t\tt.Fatal(\"expected hasUpdate to be true\")\n\t}\n\tif latest != \"2.0.0\" {\n\t\tt.Fatalf(\"expected latest to be 2.0.0, got %s\", latest)\n\t}\n}\n\nfunc TestCheckForUpdate_SameVersion(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprint(w, `{\"tag_name\": \"v1.0.0\"}`)\n\t}))\n\tdefer server.Close()\n\n\treleaseURL = server.URL\n\n\t_, hasUpdate := CheckForUpdate(\"1.0.0\")\n\tif hasUpdate {\n\t\tt.Fatal(\"expected hasUpdate to be false for same version\")\n\t}\n}\n\nfunc TestCheckForUpdate_OlderVersion(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprint(w, `{\"tag_name\": \"v0.9.0\"}`)\n\t}))\n\tdefer server.Close()\n\n\treleaseURL = server.URL\n\n\t_, hasUpdate := CheckForUpdate(\"1.0.0\")\n\tif hasUpdate {\n\t\tt.Fatal(\"expected hasUpdate to be false for older version\")\n\t}\n}\n\nfunc TestCheckForUpdate_DevVersion(t *testing.T) {\n\t_, hasUpdate := CheckForUpdate(\"DEV\")\n\tif hasUpdate {\n\t\tt.Fatal(\"expected hasUpdate to be false for DEV version\")\n\t}\n}\n\nfunc TestCheckForUpdate_NonReleaseVersionSkipsLookup(t *testing.T) {\n\trequestCount := 0\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\trequestCount++\n\t\tfmt.Fprint(w, `{\"tag_name\": \"v9.9.9\"}`)\n\t}))\n\tdefer server.Close()\n\n\treleaseURL = server.URL\n\n\t_, hasUpdate := CheckForUpdate(\"v3.1.0-12-gabc1234\")\n\tif hasUpdate {\n\t\tt.Fatal(\"expected hasUpdate to be false for non-release version\")\n\t}\n\tif requestCount != 0 {\n\t\tt.Fatalf(\"expected no release lookup for non-release version, got %d requests\", requestCount)\n\t}\n}\n\nfunc TestCheckForUpdate_ServerError(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t}))\n\tdefer server.Close()\n\n\treleaseURL = server.URL\n\n\t_, hasUpdate := CheckForUpdate(\"1.0.0\")\n\tif hasUpdate {\n\t\tt.Fatal(\"expected hasUpdate to be false on server error\")\n\t}\n}\n\nfunc TestCheckForUpdate_InvalidJSON(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprint(w, `not json`)\n\t}))\n\tdefer server.Close()\n\n\treleaseURL = server.URL\n\n\t_, hasUpdate := CheckForUpdate(\"1.0.0\")\n\tif hasUpdate {\n\t\tt.Fatal(\"expected hasUpdate to be false on invalid JSON\")\n\t}\n}\n\nfunc TestCheckForUpdate_ServerDown(t *testing.T) {\n\treleaseURL = \"http://127.0.0.1:1\" // nothing listening\n\n\t_, hasUpdate := CheckForUpdate(\"1.0.0\")\n\tif hasUpdate {\n\t\tt.Fatal(\"expected hasUpdate to be false when server is unreachable\")\n\t}\n}\n\nfunc TestIsNewer(t *testing.T) {\n\ttests := []struct {\n\t\ta, b string\n\t\twant bool\n\t}{\n\t\t{\"2.0.0\", \"1.0.0\", true},\n\t\t{\"1.1.0\", \"1.0.0\", true},\n\t\t{\"1.0.1\", \"1.0.0\", true},\n\t\t{\"1.0.0\", \"1.0.0\", false},\n\t\t{\"0.9.0\", \"1.0.0\", false},\n\t\t{\"1.0.0\", \"1.0.1\", false},\n\t\t{\"10.0.0\", \"9.0.0\", true},\n\t\t{\"1.10.0\", \"1.9.0\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(fmt.Sprintf(\"%s_vs_%s\", tt.a, tt.b), func(t *testing.T) {\n\t\t\tgot := isNewer(tt.a, tt.b)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"isNewer(%q, %q) = %v, want %v\", tt.a, tt.b, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseVersion(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\tvalid bool\n\t}{\n\t\t{\"1.2.3\", true},\n\t\t{\"0.0.0\", true},\n\t\t{\"10.20.30\", true},\n\t\t{\"1.2\", false},\n\t\t{\"1.2.3.4\", false},\n\t\t{\"a.b.c\", false},\n\t\t{\"\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tresult := parseVersion(tt.input)\n\t\t\tif tt.valid && result == nil {\n\t\t\t\tt.Errorf(\"parseVersion(%q) returned nil, expected valid\", tt.input)\n\t\t\t}\n\t\t\tif !tt.valid && result != nil {\n\t\t\t\tt.Errorf(\"parseVersion(%q) returned %v, expected nil\", tt.input, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/version/version.go",
    "content": "package version\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nvar Version = \"DEV\"\n\ntype VersionCmd struct{}\n\nfunc (c *VersionCmd) Run() error {\n\tfmt.Fprintf(os.Stdout, \"%s\\n\", Format(Version))\n\n\tif latest, ok := CheckForUpdate(Version); ok {\n\t\tfmt.Fprint(os.Stderr, FormatUpdateNudge(latest))\n\t}\n\n\treturn nil\n}\n\nfunc Format(ver string) string {\n\tver = strings.TrimPrefix(ver, \"v\")\n\treturn fmt.Sprintf(\"bk version %s\\n\", ver)\n}\n"
  },
  {
    "path": "cmd/whoami/whoami.go",
    "content": "package whoami\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\t\"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype WhoAmIOutput struct {\n\tOrganizationSlug string                `json:\"organization_slug\"`\n\tToken            buildkite.AccessToken `json:\"token\"`\n}\n\nfunc (w WhoAmIOutput) TextOutput() string {\n\tb := strings.Builder{}\n\n\tfmt.Fprintf(&b, \"Current organization: %s\\n\", w.OrganizationSlug)\n\tb.WriteRune('\\n')\n\tfmt.Fprintf(&b, \"API Token UUID:        %s\\n\", w.Token.UUID)\n\tfmt.Fprintf(&b, \"API Token Description: %s\\n\", w.Token.Description)\n\tfmt.Fprintf(&b, \"API Token Scopes:      %v\\n\", w.Token.Scopes)\n\tb.WriteRune('\\n')\n\tfmt.Fprintf(&b, \"API Token user name:  %s\\n\", w.Token.User.Name)\n\tfmt.Fprintf(&b, \"API Token user email: %s\\n\", w.Token.User.Email)\n\n\treturn b.String()\n}\n\ntype WhoAmICmd struct {\n\toutput.OutputFlags\n}\n\nfunc (c *WhoAmICmd) Help() string {\n\treturn `\nIt returns information on the current session.\n\nExamples:\n\t# List the current token session\n\t$ bk whoami\n\n\t# List the current token session in JSON format\n\t$ bk whoami -o json\n`\n}\n\nfunc (c *WhoAmICmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error {\n\tf, err := factory.New(factory.WithDebug(globals.EnableDebug()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := validation.ValidateConfiguration(f.Config, kongCtx.Command()); err != nil {\n\t\treturn err\n\t}\n\n\tformat := output.ResolveFormat(c.Output, f.Config.OutputFormat())\n\n\tctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\n\tdefer stop()\n\n\torgSlug := f.Config.OrganizationSlug()\n\n\tif orgSlug == \"\" {\n\t\torgSlug = \"<None>\"\n\t}\n\n\ttoken, _, err := f.RestAPIClient.AccessTokens.Get(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get access token: %w\", err)\n\t}\n\n\tw := WhoAmIOutput{\n\t\tOrganizationSlug: orgSlug,\n\t\tToken:            token,\n\t}\n\n\terr = output.Write(os.Stdout, w, format)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write output: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "docs/shell-prompt-integration.md",
    "content": "### Shell Prompt Integration\n\nThe Buildkite CLI offers a shell prompt integration that displays your current Buildkite organization directly in your prompt.\n\n![bk cli prompt](./images/prompt.png)\n\n#### Zsh (Vanilla)\n\n1. Create a prompt function in `~/.buildkite/zsh_prompt.zsh`:\n\n```zsh\n_buildkite_ps1() {\n    local org=$(bk use 2>&1 | grep \"Using configuration for\" | sed -E \"s/Using configuration for \\`(.*)\\`/\\1/\")\n    if [[ -n \"$org\" ]]; then\n        echo -n \" (bk:$org)\"\n    fi\n}\n\n# Modify your existing prompt to include the Buildkite organization\nPROMPT='%n@%m %1~$(_buildkite_ps1)%# '\n```\n\n2. Source the script in your `.zshrc`:\n\n```zsh\nsource $HOME/.buildkite/zsh_prompt.zsh\n```\n\n#### Zsh (Powerlevel10k)\n\n1. Add the following function to `~/.buildkite/zsh_prompt.zsh`:\n\n```zsh\n_buildkite_ps1() {\n    # Cache the prompt output for 5 seconds to avoid running bk too frequently\n    if [[ -z \"$BK_PROMPT_CACHE\" ]] || [[ $(($EPOCHSECONDS % 5)) -eq 0 ]]; then\n        local org=$(bk use 2>&1 | grep \"Using configuration for\" | sed -E \"s/Using configuration for \\`(.*)\\`/\\1/\")\n        if [[ -n \"$org\" ]]; then\n            BK_PROMPT_CACHE=\"%F{magenta}(bk:$org)%f\"\n        else\n            BK_PROMPT_CACHE=\"%F{yellow}(bk:not configured)%f\"\n        fi\n    fi\n    echo -n \"$BK_PROMPT_CACHE\"\n}\n\n# Wrap the bk command to clear prompt cache when switching orgs\nbk() {\n    command bk \"$@\"\n    if [[ \"$1\" == \"use\" ]]; then\n        unset BK_PROMPT_CACHE\n    fi\n}\n```\n\n2. Source this script in your `.zshrc`:\n\n```zsh\nsource $HOME/.buildkite/zsh_prompt.zsh\n```\n\n3. Add the Buildkite organization to your prompt elements in `~/.p10k.zsh`:\n\n```zsh\ntypeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(\n  # ... other existing elements\n  buildkite_org\n)\n```\n\n#### Bash\n\n1. Create a prompt function in `~/.buildkite/bash_prompt.sh`:\n\n```bash\n_buildkite_ps1() {\n    local org=$(bk use 2>&1 | grep \"Using configuration for\" | sed -E \"s/Using configuration for \\`(.*)\\`/\\1/\")\n    if [[ -n \"$org\" ]]; then\n        echo -n \" (bk:$org)\"\n    fi\n}\n\n# Modify your PS1 to include the Buildkite organization\nexport PS1='\\u@\\h \\w$(_buildkite_ps1)\\$ '\n```\n\n2. Source the script in your `.bashrc`:\n\n```bash\nsource $HOME/.buildkite/bash_prompt.sh\n```\n\n#### Features\n\n- Displays current Buildkite organization in your shell prompt\n- Caches organization info to minimize performance impact\n- Works across different projects and directories\n- Supports quick organization switching with `bk use`\n\n#### Troubleshooting\n\n- Ensure you've run `bk configure` to set up your organization\n- Verify the `.bk.yaml` in your project's root directory\n- Check that you're using a locally built `bk` binary in development projects\n\n#### Performance Considerations\n\nThe prompt integration uses a lightweight method to retrieve the current organization. However, to minimize any potential performance impact:\n- The script caches the organization name\n- The command is only run periodically or when switching organizations\n- You can customize the caching mechanism if needed\n"
  },
  {
    "path": "fixtures/build.json",
    "content": "[\n  {\n    \"id\": \"018f7ee5-xxxx-429f-83ad-89e3b6c6c3ef\",\n    \"graphql_id\": \"QnVpbGQtLS0wxxxxxxxxxxxxNTkyLTQyOWYtODNhZC04OWUzYjZjNmMzZWY=\",\n    \"url\": \"https://api.buildkite.com/v2/organizations/buildkite/pipelines/buildkite-cli/builds/xx\",\n    \"web_url\": \"https://buildkite.com/buildkite/buildkite-cli/builds/xx\",\n    \"number\": 584,\n    \"state\": \"passed\",\n    \"cancel_reason\": null,\n    \"blocked\": false,\n    \"blocked_state\": \"\",\n    \"message\": \"\",\n    \"commit\": \"\",\n    \"branch\": \"\",\n    \"tag\": null,\n    \"env\": {},\n    \"source\": \"webhook\",\n    \"author\": {},\n    \"creator\": {\n      \"id\": \"0183c4e6-c88c-xxxx-b15e-7801077a9181\",\n      \"graphql_id\": \"VXNlci0tLTAxODNjNGU2LWM4OGxxxxxxxxxiMTVlLTc4MDEwNzdhOTE4MQ==\"\n    },\n    \"created_at\": \"2024-05-16T00:55:26.401Z\",\n    \"scheduled_at\": \"2024-05-16T00:55:26.256Z\",\n    \"started_at\": \"2024-05-16T00:55:30.670Z\",\n    \"finished_at\": \"2024-05-16T00:58:58.887Z\",\n    \"meta_data\": {},\n    \"pull_request\": {},\n    \"rebuilt_from\": null,\n    \"pipeline\": {\n      \"id\": \"26e6a5b3-xxxx-4be3-8f2a-db21faf06597\",\n      \"graphql_id\": \"UGlwZWxpxxxxxxxxxxU2YTViMy1hZDExLTRiZTMtOGYyYS1kYjIxZmFmMDY1OTc=\",\n      \"url\": \"https://api.buildkite.com/v2/organizations/buildkite/pipelines/buildkite-cli\",\n      \"web_url\": \"https://buildkite.com/buildkite/buildkite-cli\",\n      \"name\": \"Buildkite CLI\",\n      \"description\": \"A command line interface for Buildkite.\",\n      \"slug\": \"buildkite-cli\",\n      \"repository\": \"https://github.com/buildkite/cli.git\",\n      \"cluster_id\": \"5f0748b7\",\n      \"pipeline_template_uuid\": null,\n      \"branch_configuration\": null,\n      \"default_branch\": \"3.x\",\n      \"skip_queued_branch_builds\": true,\n      \"skip_queued_branch_builds_filter\": \"\",\n      \"cancel_running_branch_builds\": true,\n      \"cancel_running_branch_builds_filter\": \"\",\n      \"allow_rebuilds\": true,\n      \"provider\": {},\n      \"builds_url\": \"https://api.buildkite.com/v2/organizations/buildkite/pipelines/buildkite-cli/builds\",\n      \"badge_url\": \"https://badge.buildkite.com/01e74a00a1f107ee6ee736731541ebb4982b923ff872b46f41.svg\",\n      \"created_by\": {},\n      \"created_at\": \"2020-07-06T05:40:03.165Z\",\n      \"archived_at\": null,\n      \"env\": null,\n      \"scheduled_builds_count\": 0,\n      \"running_builds_count\": 0,\n      \"scheduled_jobs_count\": 0,\n      \"running_jobs_count\": 1,\n      \"waiting_jobs_count\": 0,\n      \"visibility\": \"public\",\n      \"tags\": [\n        \":console:\",\n        \":golang:\"\n      ],\n      \"emoji\": \":console:\",\n      \"color\": \"#CAD3F5\",\n      \"configuration\": \"\",\n      \"steps\": [],\n      \"cluster_url\": \"\"\n    },\n    \"jobs\": [],\n    \"cluster_id\": \"\",\n    \"cluster_url\": \"\"\n  }\n]\n"
  },
  {
    "path": "fixtures/config/local.basic.yaml",
    "content": "selected_org: buildkite-test\norganizations:\n  buildkite-test:\n    api_token: test-token-1234\npipelines:\n  - first-pipeline\n  - second-pipeline\n"
  },
  {
    "path": "fixtures/config/user.basic.yaml",
    "content": "selected_org: buildkite-org\norganizations:\n  buildkite-test:\n    api_token: test-token-abcd\n"
  },
  {
    "path": "genqlient.yaml",
    "content": "schema: schema.graphql\n\noperations:\n  - internal/**/*.graphql\n  - cmd/**/*.graphql\n\noptional: pointer\n\ngenerated: internal/graphql/generated.go\n\nbindings:\n  JSON:\n    type: string\n  YAML:\n    type: string\n  DateTime:\n    type: time.Time\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/buildkite/cli/v3\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/alecthomas/kong v1.15.0\n\tgithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be\n\tgithub.com/buildkite/go-buildkite/v4 v4.22.0\n\tgithub.com/buildkite/termoji v0.0.0-20260330080310-c0aa4ebee0d1\n\tgithub.com/charmbracelet/bubbles v1.0.0\n\tgithub.com/charmbracelet/bubbletea v1.3.10\n\tgithub.com/charmbracelet/lipgloss v1.1.0\n\tgithub.com/charmbracelet/x/ansi v0.11.7\n\tgithub.com/go-git/go-git/v5 v5.19.0\n\tgithub.com/goccy/go-yaml v1.19.2\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c\n\tgithub.com/posthog/posthog-go v1.12.5\n\tgithub.com/vektah/gqlparser/v2 v2.5.33\n\tgithub.com/xeipuuv/gojsonschema v1.2.0\n\tgithub.com/zalando/go-keyring v0.2.8\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tgithub.com/agnivade/levenshtein v1.2.1 // indirect\n\tgithub.com/alexflint/go-arg v1.5.1 // indirect\n\tgithub.com/alexflint/go-scalar v1.2.0 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/bmatcuk/doublestar/v4 v4.6.1 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.4.1 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.15 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.2 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.11.0 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.7.0 // indirect\n\tgithub.com/danieljoos/wincred v1.2.3 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/goccy/go-json v0.10.5 // indirect\n\tgithub.com/godbus/dbus/v5 v5.2.2 // indirect\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/kyokomi/emoji/v2 v2.2.13 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.4.0 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/muesli/termenv v0.16.0 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n)\n\nrequire (\n\tdario.cat/mergo v1.0.0 // indirect\n\tgithub.com/Khan/genqlient v0.8.1\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/ProtonMail/go-crypto v1.1.6 // indirect\n\tgithub.com/cenkalti/backoff v2.2.1+incompatible // indirect\n\tgithub.com/cloudflare/circl v1.6.3 // indirect\n\tgithub.com/cyphar/filepath-securejoin v0.6.1 // indirect\n\tgithub.com/emirpasic/gods v1.18.1 // indirect\n\tgithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect\n\tgithub.com/go-git/go-billy/v5 v5.9.0 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/google/go-querystring v1.2.0 // indirect\n\tgithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect\n\tgithub.com/kevinburke/ssh_config v1.2.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.22\n\tgithub.com/mattn/go-runewidth v0.0.23\n\tgithub.com/pjbgf/sha1cd v0.6.0 // indirect\n\tgithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect\n\tgithub.com/skeema/knownhosts v1.3.1 // indirect\n\tgithub.com/spf13/afero v1.15.0\n\tgithub.com/suessflorian/gqlfetch v0.7.0\n\tgithub.com/xanzy/ssh-agent v0.3.3 // indirect\n\tgolang.org/x/crypto v0.50.0 // indirect\n\tgolang.org/x/mod v0.34.0 // indirect\n\tgolang.org/x/net v0.53.0 // indirect\n\tgolang.org/x/sys v0.44.0 // indirect\n\tgolang.org/x/term v0.43.0\n\tgolang.org/x/text v0.36.0 // indirect\n\tgolang.org/x/tools v0.43.0 // indirect\n\tgopkg.in/warnings.v0 v0.1.2 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=\ndario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=\ngithub.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=\ngithub.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=\ngithub.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=\ngithub.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=\ngithub.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=\ngithub.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=\ngithub.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=\ngithub.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=\ngithub.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI=\ngithub.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=\ngithub.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=\ngithub.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=\ngithub.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=\ngithub.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=\ngithub.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=\ngithub.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=\ngithub.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=\ngithub.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=\ngithub.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs=\ngithub.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=\ngithub.com/buildkite/go-buildkite/v4 v4.22.0 h1:S0jDYBh4iUAx0J5VYrJe+VdVTgvZVCrD9rqOs2l0xCk=\ngithub.com/buildkite/go-buildkite/v4 v4.22.0/go.mod h1:t/M4DUcs7qyebtzm3nkyZ1zUB/svWnKtR+uRU2Ca8tQ=\ngithub.com/buildkite/termoji v0.0.0-20260330080310-c0aa4ebee0d1 h1:aaEl0QZURcwC+KOfFTzSp66xknw5eTmFZ1NgB87s2xk=\ngithub.com/buildkite/termoji v0.0.0-20260330080310-c0aa4ebee0d1/go.mod h1:ZTEvQlMN3+qzjROvjRb1p0X+xDQxxKpkMFhMSnaTrpw=\ngithub.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=\ngithub.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=\ngithub.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=\ngithub.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=\ngithub.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=\ngithub.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=\ngithub.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=\ngithub.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=\ngithub.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=\ngithub.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=\ngithub.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=\ngithub.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=\ngithub.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=\ngithub.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=\ngithub.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=\ngithub.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=\ngithub.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=\ngithub.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=\ngithub.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=\ngithub.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=\ngithub.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=\ngithub.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=\ngithub.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=\ngithub.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=\ngithub.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=\ngithub.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=\ngithub.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=\ngithub.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=\ngithub.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=\ngithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=\ngithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=\ngithub.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=\ngithub.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=\ngithub.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=\ngithub.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=\ngithub.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=\ngithub.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=\ngithub.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=\ngithub.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=\ngithub.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=\ngithub.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=\ngithub.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=\ngithub.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=\ngithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=\ngithub.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=\ngithub.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=\ngithub.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=\ngithub.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=\ngithub.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=\ngithub.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=\ngithub.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=\ngithub.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=\ngithub.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=\ngithub.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=\ngithub.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posthog/posthog-go v1.12.5 h1:l/x3mpqisXJ0sTOyyRutsTQAgiWYuJT1uhN4cQraJ8o=\ngithub.com/posthog/posthog-go v1.12.5/go.mod h1:xsVOW9YImilUcazwPNEq4PJDqEZf2KeCS758zXjwkPg=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=\ngithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=\ngithub.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/suessflorian/gqlfetch v0.7.0 h1:lh33oml4koA2xzIqeW8hxBCPCHC5c25K1VEP4LD5gGg=\ngithub.com/suessflorian/gqlfetch v0.7.0/go.mod h1:Q6tGWULnU3Lj5yBWVZSoabHAmIftaGrw1BuboWqrnf8=\ngithub.com/vektah/gqlparser/v2 v2.5.33 h1:lRp8aIeNUNbimf/axZd7ETg24q06hBtPaas+TcvI/7E=\ngithub.com/vektah/gqlparser/v2 v2.5.33/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=\ngithub.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=\ngithub.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=\ngithub.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=\ngolang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=\ngolang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=\ngolang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=\ngolang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "internal/agent/download.go",
    "content": "package agent\n\nimport (\n\t\"archive/tar\"\n\t\"archive/zip\"\n\t\"compress/gzip\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// ExistingInstall describes a buildkite-agent binary already present on the system.\ntype ExistingInstall struct {\n\tPath    string\n\tVersion string\n}\n\n// FindExisting looks for buildkite-agent in PATH and returns info about it.\n// Returns nil if no existing installation is found.\nfunc FindExisting(targetOS string) *ExistingInstall {\n\tname := BinaryName(targetOS)\n\tpath, err := exec.LookPath(name)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tinstall := &ExistingInstall{Path: path}\n\n\tout, err := exec.Command(path, \"--version\").Output()\n\tif err == nil {\n\t\tversion := strings.TrimSpace(string(out))\n\t\t// Output is like \"buildkite-agent version 3.119.2+11755.abc123...\"\n\t\t// Extract just the semver portion.\n\t\tversion = strings.TrimPrefix(version, \"buildkite-agent version \")\n\t\tif plusIdx := strings.Index(version, \"+\"); plusIdx != -1 {\n\t\t\tversion = version[:plusIdx]\n\t\t}\n\t\tinstall.Version = version\n\t}\n\n\treturn install\n}\n\n// ResolveLatestVersion queries the GitHub API for the latest buildkite-agent release tag.\nfunc ResolveLatestVersion() (string, error) {\n\treq, err := http.NewRequest(\"GET\", \"https://api.github.com/repos/buildkite/agent/releases/latest\", nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header.Set(\"Accept\", \"application/vnd.github+json\")\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"GitHub API returned status %d\", resp.StatusCode)\n\t}\n\n\tvar release struct {\n\t\tTagName string `json:\"tag_name\"`\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&release); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn strings.TrimPrefix(release.TagName, \"v\"), nil\n}\n\n// BuildDownloadURL returns the GitHub releases URL for the given agent version, OS, and arch.\nfunc BuildDownloadURL(version, os, arch string) string {\n\tvar extension string\n\tswitch os {\n\tcase \"windows\":\n\t\textension = \"zip\"\n\tdefault:\n\t\textension = \"tar.gz\"\n\t}\n\n\treturn fmt.Sprintf(\n\t\t\"https://github.com/buildkite/agent/releases/download/v%s/buildkite-agent-%s-%s-%s.%s\",\n\t\tversion, os, arch, version, extension,\n\t)\n}\n\n// BuildSHA256SumsURL returns the URL for the SHA256SUMS file for a given agent version.\nfunc BuildSHA256SumsURL(version string) string {\n\treturn fmt.Sprintf(\n\t\t\"https://github.com/buildkite/agent/releases/download/v%s/buildkite-agent-%s.SHA256SUMS\",\n\t\tversion, version,\n\t)\n}\n\n// FetchExpectedSHA256 downloads the SHA256SUMS file and returns the expected hash\n// for the given archive filename.\nfunc FetchExpectedSHA256(sumsURL, archiveFilename string) (string, error) {\n\tresp, err := http.Get(sumsURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"fetching SHA256SUMS failed with status %d\", resp.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, line := range strings.Split(strings.TrimSpace(string(body)), \"\\n\") {\n\t\t// Format: \"<hash>  <filename>\"\n\t\tparts := strings.SplitN(line, \"  \", 2)\n\t\tif len(parts) == 2 && parts[1] == archiveFilename {\n\t\t\treturn parts[0], nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no SHA256 checksum found for %s\", archiveFilename)\n}\n\n// VerifySHA256 computes the SHA256 hash of the file at path and compares it\n// to the expected hex-encoded hash. Returns an error if they don't match.\nfunc VerifySHA256(path, expected string) error {\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\n\th := sha256.New()\n\tif _, err := io.Copy(h, f); err != nil {\n\t\treturn err\n\t}\n\n\tactual := hex.EncodeToString(h.Sum(nil))\n\tif actual != expected {\n\t\treturn fmt.Errorf(\"SHA256 mismatch: expected %s, got %s\", expected, actual)\n\t}\n\n\treturn nil\n}\n\n// DownloadToTemp downloads the given URL to a temporary file and returns its path.\n// The caller is responsible for removing the file when done.\nfunc DownloadToTemp(url string) (string, error) {\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"download failed with status %d\", resp.StatusCode)\n\t}\n\n\ttmpFile, err := os.CreateTemp(\"\", \"buildkite-agent-*\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer tmpFile.Close()\n\n\tif _, err := io.Copy(tmpFile, resp.Body); err != nil {\n\t\tos.Remove(tmpFile.Name())\n\t\treturn \"\", err\n\t}\n\n\treturn tmpFile.Name(), nil\n}\n\n// ExtractBinary extracts the buildkite-agent binary from the given archive to dest.\nfunc ExtractBinary(archive, dest, targetOS string) error {\n\tif targetOS == \"windows\" {\n\t\treturn extractZip(archive, dest)\n\t}\n\treturn extractTarGz(archive, dest)\n}\n\n// BinaryName returns the platform-appropriate binary name.\nfunc BinaryName(targetOS string) string {\n\tif targetOS == \"windows\" {\n\t\treturn \"buildkite-agent.exe\"\n\t}\n\treturn \"buildkite-agent\"\n}\n\nfunc extractTarGz(archive, dest string) error {\n\tf, err := os.Open(archive)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\n\tgz, err := gzip.NewReader(f)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer gz.Close()\n\n\ttr := tar.NewReader(gz)\n\tfor {\n\t\theader, err := tr.Next()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif filepath.Base(header.Name) != \"buildkite-agent\" {\n\t\t\tcontinue\n\t\t}\n\n\t\toutPath := filepath.Join(dest, \"buildkite-agent\")\n\t\tout, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer out.Close()\n\n\t\tif _, err := io.Copy(out, tr); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"buildkite-agent binary not found in archive\")\n}\n\nfunc extractZip(archive, dest string) error {\n\tr, err := zip.OpenReader(archive)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer r.Close()\n\n\tfor _, f := range r.File {\n\t\tif filepath.Base(f.Name) != \"buildkite-agent.exe\" {\n\t\t\tcontinue\n\t\t}\n\n\t\trc, err := f.Open()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer rc.Close()\n\n\t\toutPath := filepath.Join(dest, \"buildkite-agent.exe\")\n\t\tout, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer out.Close()\n\n\t\tif _, err := io.Copy(out, rc); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"buildkite-agent.exe not found in archive\")\n}\n"
  },
  {
    "path": "internal/agent/download_test.go",
    "content": "package agent\n\nimport (\n\t\"archive/tar\"\n\t\"archive/zip\"\n\t\"compress/gzip\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestBuildDownloadURL(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname    string\n\t\tversion string\n\t\tos      string\n\t\tarch    string\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tname:    \"linux amd64\",\n\t\t\tversion: \"3.120.0\",\n\t\t\tos:      \"linux\",\n\t\t\tarch:    \"amd64\",\n\t\t\twant:    \"https://github.com/buildkite/agent/releases/download/v3.120.0/buildkite-agent-linux-amd64-3.120.0.tar.gz\",\n\t\t},\n\t\t{\n\t\t\tname:    \"darwin arm64\",\n\t\t\tversion: \"3.120.0\",\n\t\t\tos:      \"darwin\",\n\t\t\tarch:    \"arm64\",\n\t\t\twant:    \"https://github.com/buildkite/agent/releases/download/v3.120.0/buildkite-agent-darwin-arm64-3.120.0.tar.gz\",\n\t\t},\n\t\t{\n\t\t\tname:    \"windows amd64\",\n\t\t\tversion: \"3.120.0\",\n\t\t\tos:      \"windows\",\n\t\t\tarch:    \"amd64\",\n\t\t\twant:    \"https://github.com/buildkite/agent/releases/download/v3.120.0/buildkite-agent-windows-amd64-3.120.0.zip\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := BuildDownloadURL(tt.version, tt.os, tt.arch)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"BuildDownloadURL() = %q, want %q\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBuildSHA256SumsURL(t *testing.T) {\n\tt.Parallel()\n\n\tgot := BuildSHA256SumsURL(\"3.120.0\")\n\twant := \"https://github.com/buildkite/agent/releases/download/v3.120.0/buildkite-agent-3.120.0.SHA256SUMS\"\n\tif got != want {\n\t\tt.Errorf(\"BuildSHA256SumsURL() = %q, want %q\", got, want)\n\t}\n}\n\nfunc TestFetchExpectedSHA256(t *testing.T) {\n\tt.Parallel()\n\n\tsumsBody := \"abc123  buildkite-agent-linux-amd64-3.120.0.tar.gz\\ndef456  buildkite-agent-darwin-arm64-3.120.0.tar.gz\\n\"\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprint(w, sumsBody)\n\t}))\n\tt.Cleanup(server.Close)\n\n\ttests := []struct {\n\t\tname     string\n\t\tfilename string\n\t\twant     string\n\t\twantErr  bool\n\t}{\n\t\t{\"found linux\", \"buildkite-agent-linux-amd64-3.120.0.tar.gz\", \"abc123\", false},\n\t\t{\"found darwin\", \"buildkite-agent-darwin-arm64-3.120.0.tar.gz\", \"def456\", false},\n\t\t{\"not found\", \"buildkite-agent-windows-amd64-3.120.0.zip\", \"\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot, err := FetchExpectedSHA256(server.URL, tt.filename)\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"FetchExpectedSHA256() = %q, want %q\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVerifySHA256(t *testing.T) {\n\tt.Parallel()\n\n\tcontent := []byte(\"hello buildkite agent\")\n\thash := sha256.Sum256(content)\n\texpectedHex := hex.EncodeToString(hash[:])\n\n\ttmpFile := filepath.Join(t.TempDir(), \"testfile\")\n\tif err := os.WriteFile(tmpFile, content, 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Should pass with correct hash\n\tif err := VerifySHA256(tmpFile, expectedHex); err != nil {\n\t\tt.Errorf(\"VerifySHA256() with correct hash: unexpected error: %v\", err)\n\t}\n\n\t// Should fail with wrong hash\n\terr := VerifySHA256(tmpFile, \"0000000000000000000000000000000000000000000000000000000000000000\")\n\tif err == nil {\n\t\tt.Fatal(\"VerifySHA256() with wrong hash: expected error, got nil\")\n\t}\n\tif got := err.Error(); !strings.Contains(got, \"SHA256 mismatch\") {\n\t\tt.Errorf(\"unexpected error message: %s\", got)\n\t}\n}\n\nfunc TestBinaryName(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tos   string\n\t\twant string\n\t}{\n\t\t{\"linux\", \"buildkite-agent\"},\n\t\t{\"darwin\", \"buildkite-agent\"},\n\t\t{\"windows\", \"buildkite-agent.exe\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.os, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := BinaryName(tt.os)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"BinaryName(%q) = %q, want %q\", tt.os, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestExtractTarGz(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a tar.gz archive containing a fake buildkite-agent binary\n\tarchivePath := filepath.Join(t.TempDir(), \"agent.tar.gz\")\n\tbinaryContent := []byte(\"#!/bin/sh\\necho hello\\n\")\n\n\tf, err := os.Create(archivePath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tgw := gzip.NewWriter(f)\n\ttw := tar.NewWriter(gw)\n\n\t// Add a non-matching file first\n\tif err := tw.WriteHeader(&tar.Header{Name: \"README.md\", Size: 5, Mode: 0o644}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := tw.Write([]byte(\"hello\")); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Add the buildkite-agent binary\n\tif err := tw.WriteHeader(&tar.Header{Name: \"buildkite-agent\", Size: int64(len(binaryContent)), Mode: 0o755}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := tw.Write(binaryContent); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttw.Close()\n\tgw.Close()\n\tf.Close()\n\n\t// Extract to a temp dir\n\tdest := t.TempDir()\n\tif err := extractTarGz(archivePath, dest); err != nil {\n\t\tt.Fatalf(\"extractTarGz() error: %v\", err)\n\t}\n\n\t// Verify the binary was extracted\n\textracted, err := os.ReadFile(filepath.Join(dest, \"buildkite-agent\"))\n\tif err != nil {\n\t\tt.Fatalf(\"reading extracted binary: %v\", err)\n\t}\n\tif string(extracted) != string(binaryContent) {\n\t\tt.Errorf(\"extracted content = %q, want %q\", extracted, binaryContent)\n\t}\n\n\t// Verify README.md was NOT extracted\n\tif _, err := os.Stat(filepath.Join(dest, \"README.md\")); !os.IsNotExist(err) {\n\t\tt.Error(\"README.md should not have been extracted\")\n\t}\n}\n\nfunc TestExtractTarGz_MissingBinary(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a tar.gz with no buildkite-agent file\n\tarchivePath := filepath.Join(t.TempDir(), \"agent.tar.gz\")\n\n\tf, err := os.Create(archivePath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tgw := gzip.NewWriter(f)\n\ttw := tar.NewWriter(gw)\n\n\tif err := tw.WriteHeader(&tar.Header{Name: \"other-file\", Size: 5, Mode: 0o644}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := tw.Write([]byte(\"hello\")); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttw.Close()\n\tgw.Close()\n\tf.Close()\n\n\tdest := t.TempDir()\n\terr = extractTarGz(archivePath, dest)\n\tif err == nil {\n\t\tt.Fatal(\"expected error for missing binary, got nil\")\n\t}\n\tif err.Error() != \"buildkite-agent binary not found in archive\" {\n\t\tt.Errorf(\"unexpected error: %v\", err)\n\t}\n}\n\nfunc TestExtractZip(t *testing.T) {\n\tt.Parallel()\n\n\tarchivePath := filepath.Join(t.TempDir(), \"agent.zip\")\n\tbinaryContent := []byte(\"fake-exe-content\")\n\n\tf, err := os.Create(archivePath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tzw := zip.NewWriter(f)\n\n\t// Add a non-matching file\n\tw, err := zw.Create(\"README.md\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write([]byte(\"hello\")); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Add the binary\n\tw, err = zw.Create(\"buildkite-agent.exe\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(binaryContent); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tzw.Close()\n\tf.Close()\n\n\tdest := t.TempDir()\n\tif err := extractZip(archivePath, dest); err != nil {\n\t\tt.Fatalf(\"extractZip() error: %v\", err)\n\t}\n\n\textracted, err := os.ReadFile(filepath.Join(dest, \"buildkite-agent.exe\"))\n\tif err != nil {\n\t\tt.Fatalf(\"reading extracted binary: %v\", err)\n\t}\n\tif string(extracted) != string(binaryContent) {\n\t\tt.Errorf(\"extracted content = %q, want %q\", extracted, binaryContent)\n\t}\n\n\tif _, err := os.Stat(filepath.Join(dest, \"README.md\")); !os.IsNotExist(err) {\n\t\tt.Error(\"README.md should not have been extracted\")\n\t}\n}\n\nfunc TestExtractZip_MissingBinary(t *testing.T) {\n\tt.Parallel()\n\n\tarchivePath := filepath.Join(t.TempDir(), \"agent.zip\")\n\n\tf, err := os.Create(archivePath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tzw := zip.NewWriter(f)\n\tw, err := zw.Create(\"other-file\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write([]byte(\"hello\")); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tzw.Close()\n\tf.Close()\n\n\tdest := t.TempDir()\n\terr = extractZip(archivePath, dest)\n\tif err == nil {\n\t\tt.Fatal(\"expected error for missing binary, got nil\")\n\t}\n\tif err.Error() != \"buildkite-agent.exe not found in archive\" {\n\t\tt.Errorf(\"unexpected error: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "internal/agent/platform.go",
    "content": "package agent\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// DefaultBinDir returns the platform-appropriate default directory for the agent binary.\nfunc DefaultBinDir(targetOS string) string {\n\tswitch targetOS {\n\tcase \"windows\":\n\t\tif appData := os.Getenv(\"LOCALAPPDATA\"); appData != \"\" {\n\t\t\treturn filepath.Join(appData, \"Buildkite\", \"bin\")\n\t\t}\n\t\treturn filepath.Join(\"C:\\\\\", \"Program Files\", \"buildkite\", \"bin\")\n\tdefault:\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn \"/usr/local/bin\"\n\t\t}\n\t\treturn filepath.Join(home, \".buildkite-agent\", \"bin\")\n\t}\n}\n\n// DefaultBuildPath returns the platform-appropriate default directory for agent builds.\nfunc DefaultBuildPath(targetOS string) string {\n\tswitch targetOS {\n\tcase \"windows\":\n\t\tif appData := os.Getenv(\"LOCALAPPDATA\"); appData != \"\" {\n\t\t\treturn filepath.Join(appData, \"Buildkite\", \"builds\")\n\t\t}\n\t\treturn filepath.Join(\"C:\\\\\", \"Program Files\", \"buildkite\", \"builds\")\n\tdefault:\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn \"/var/lib/buildkite-agent/builds\"\n\t\t}\n\t\treturn filepath.Join(home, \".buildkite-agent\", \"builds\")\n\t}\n}\n\n// DefaultConfigPath returns the platform-appropriate default path for the agent config file.\nfunc DefaultConfigPath(targetOS string) string {\n\tswitch targetOS {\n\tcase \"windows\":\n\t\tif appData := os.Getenv(\"LOCALAPPDATA\"); appData != \"\" {\n\t\t\treturn filepath.Join(appData, \"Buildkite\", \"buildkite-agent.cfg\")\n\t\t}\n\t\treturn filepath.Join(\"C:\\\\\", \"Program Files\", \"buildkite\", \"buildkite-agent.cfg\")\n\tdefault:\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn \"/etc/buildkite-agent/buildkite-agent.cfg\"\n\t\t}\n\t\treturn filepath.Join(home, \".buildkite-agent\", \"buildkite-agent.cfg\")\n\t}\n}\n"
  },
  {
    "path": "internal/agent/platform_test.go",
    "content": "package agent\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestDefaultBinDir(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tos       string\n\t\tcontains string\n\t}{\n\t\t{\"linux uses .buildkite-agent\", \"linux\", \".buildkite-agent/bin\"},\n\t\t{\"darwin uses .buildkite-agent\", \"darwin\", \".buildkite-agent/bin\"},\n\t\t{\"windows uses buildkite\", \"windows\", \"buildkite\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := DefaultBinDir(tt.os)\n\t\t\tif !strings.Contains(got, tt.contains) {\n\t\t\t\tt.Errorf(\"DefaultBinDir(%q) = %q, expected to contain %q\", tt.os, got, tt.contains)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefaultBuildPath(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tos       string\n\t\tcontains string\n\t}{\n\t\t{\"linux uses .buildkite-agent\", \"linux\", \".buildkite-agent/builds\"},\n\t\t{\"darwin uses .buildkite-agent\", \"darwin\", \".buildkite-agent/builds\"},\n\t\t{\"windows uses buildkite\", \"windows\", \"buildkite\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := DefaultBuildPath(tt.os)\n\t\t\tif !strings.Contains(got, tt.contains) {\n\t\t\t\tt.Errorf(\"DefaultBuildPath(%q) = %q, expected to contain %q\", tt.os, got, tt.contains)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefaultConfigPath(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tos       string\n\t\tcontains string\n\t}{\n\t\t{\"linux uses .buildkite-agent\", \"linux\", \".buildkite-agent/buildkite-agent.cfg\"},\n\t\t{\"darwin uses .buildkite-agent\", \"darwin\", \".buildkite-agent/buildkite-agent.cfg\"},\n\t\t{\"windows uses buildkite\", \"windows\", \"buildkite\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := DefaultConfigPath(tt.os)\n\t\t\tif !strings.Contains(got, tt.contains) {\n\t\t\t\tt.Errorf(\"DefaultConfigPath(%q) = %q, expected to contain %q\", tt.os, got, tt.contains)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/agent/token.go",
    "content": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// FindCluster resolves a cluster for the given org. If clusterID is provided,\n// it is returned directly. Otherwise it looks up the \"Default\" cluster.\nfunc FindCluster(ctx context.Context, f *factory.Factory, org, clusterID string) (string, error) {\n\tif clusterID != \"\" {\n\t\treturn clusterID, nil\n\t}\n\n\tclusters, _, err := f.RestAPIClient.Clusters.List(ctx, org, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, c := range clusters {\n\t\tif c.Name == \"Default\" {\n\t\t\treturn c.ID, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"no cluster named \\\"Default\\\" found in organization %q\", org)\n}\n\n// CreateAgentToken creates an agent token on the given cluster and returns the token string.\nfunc CreateAgentToken(ctx context.Context, f *factory.Factory, org, clusterID, description string) (string, error) {\n\ttoken, _, err := f.RestAPIClient.ClusterTokens.Create(ctx, org, clusterID, buildkite.ClusterTokenCreateUpdate{\n\t\tDescription: description,\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn token.Token, nil\n}\n\n// WriteAgentConfig writes a minimal agent config file with the given token, build path,\n// and optional tags. The file is created with 0600 permissions.\nfunc WriteAgentConfig(path, token, buildPath string, tags []string) error {\n\tif err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {\n\t\treturn err\n\t}\n\n\tcontent := fmt.Sprintf(\"token=%q\\nbuild-path=%q\\n\", token, buildPath)\n\tfor _, tag := range tags {\n\t\tcontent += fmt.Sprintf(\"tags=%q\\n\", tag)\n\t}\n\treturn os.WriteFile(path, []byte(content), 0o600)\n}\n"
  },
  {
    "path": "internal/agent/token_test.go",
    "content": "package agent\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestWriteAgentConfig(t *testing.T) {\n\tt.Parallel()\n\n\tdir := t.TempDir()\n\tconfigPath := filepath.Join(dir, \"subdir\", \"buildkite-agent.cfg\")\n\n\terr := WriteAgentConfig(configPath, \"test-token-123\", \"/tmp/builds\", []string{\"queue=default\"})\n\tif err != nil {\n\t\tt.Fatalf(\"WriteAgentConfig() error: %v\", err)\n\t}\n\n\tcontent, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\tt.Fatalf(\"reading config: %v\", err)\n\t}\n\n\texpected := \"token=\\\"test-token-123\\\"\\nbuild-path=\\\"/tmp/builds\\\"\\ntags=\\\"queue=default\\\"\\n\"\n\tif string(content) != expected {\n\t\tt.Errorf(\"config content = %q, want %q\", content, expected)\n\t}\n\n\t// Verify file permissions are restrictive\n\tinfo, err := os.Stat(configPath)\n\tif err != nil {\n\t\tt.Fatalf(\"stat config: %v\", err)\n\t}\n\tif perm := info.Mode().Perm(); perm != 0o600 {\n\t\tt.Errorf(\"config permissions = %o, want 600\", perm)\n\t}\n}\n\nfunc TestWriteAgentConfig_CreatesParentDirs(t *testing.T) {\n\tt.Parallel()\n\n\tdir := t.TempDir()\n\tconfigPath := filepath.Join(dir, \"a\", \"b\", \"c\", \"buildkite-agent.cfg\")\n\n\terr := WriteAgentConfig(configPath, \"token\", \"/builds\", nil)\n\tif err != nil {\n\t\tt.Fatalf(\"WriteAgentConfig() error: %v\", err)\n\t}\n\n\tif _, err := os.Stat(configPath); os.IsNotExist(err) {\n\t\tt.Error(\"config file was not created\")\n\t}\n}\n"
  },
  {
    "path": "internal/annotation/annotation.go",
    "content": "package annotation\n\nimport \"regexp\"\n\n// StripTags removes HTML tags from a string\nfunc StripTags(html string) string {\n\tre := regexp.MustCompile(`</[^>]+>`)\n\thtml = re.ReplaceAllString(html, \"\")\n\n\tre = regexp.MustCompile(`<[^>]*>`)\n\treturn re.ReplaceAllString(html, \"\")\n}\n"
  },
  {
    "path": "internal/annotation/list.go",
    "content": "package annotation\n\nimport (\n\t\"strings\"\n\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// AnnotationSummary renders a summary of a build annotation\nfunc AnnotationSummary(annotation *buildkite.Annotation) string {\n\tif annotation == nil {\n\t\treturn \"\"\n\t}\n\n\tbody := StripTags(annotation.BodyHTML)\n\tconst maxBody = 160\n\tbodyRunes := []rune(body)\n\tif len(bodyRunes) > maxBody {\n\t\tbody = string(bodyRunes[:maxBody]) + \"...\"\n\t}\n\n\trows := [][]string{\n\t\t{\"Style\", output.ValueOrDash(annotation.Style)},\n\t\t{\"Context\", output.ValueOrDash(annotation.Context)},\n\t\t{\"Body\", output.ValueOrDash(strings.TrimSpace(body))},\n\t}\n\n\treturn output.Table(\n\t\t[]string{\"Field\", \"Value\"},\n\t\trows,\n\t\tmap[string]string{\"field\": \"dim\", \"value\": \"italic\"},\n\t)\n}\n"
  },
  {
    "path": "internal/artifact/artifact.go",
    "content": "package artifact\n\nimport \"fmt\"\n\n// FormatBytes formats bytes into human-readable format (KB, MB, GB, etc.)\nfunc FormatBytes(bytes int64) string {\n\tconst (\n\t\tKB = 1024\n\t\tMB = 1024 * KB\n\t\tGB = 1024 * MB\n\t\tTB = 1024 * GB\n\t)\n\n\tswitch {\n\tcase bytes >= TB:\n\t\treturn fmt.Sprintf(\"%.1fTB\", float64(bytes)/TB)\n\tcase bytes >= GB:\n\t\treturn fmt.Sprintf(\"%.1fGB\", float64(bytes)/GB)\n\tcase bytes >= MB:\n\t\treturn fmt.Sprintf(\"%.1fMB\", float64(bytes)/MB)\n\tcase bytes >= KB:\n\t\treturn fmt.Sprintf(\"%.1fKB\", float64(bytes)/KB)\n\tdefault:\n\t\treturn fmt.Sprintf(\"%dB\", bytes)\n\t}\n}\n"
  },
  {
    "path": "internal/artifact/view.go",
    "content": "package artifact\n\nimport (\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// ArtifactSummary renders a summary of a build artifact\nfunc ArtifactSummary(artifact *buildkite.Artifact) string {\n\tif artifact == nil {\n\t\treturn \"\"\n\t}\n\n\trows := [][]string{{artifact.ID, artifact.Path, FormatBytes(artifact.FileSize)}}\n\n\treturn output.Table(\n\t\t[]string{\"ID\", \"Path\", \"Size\"},\n\t\trows,\n\t\tmap[string]string{\"id\": \"dim\", \"path\": \"bold\", \"size\": \"dim\"},\n\t)\n}\n"
  },
  {
    "path": "internal/build/build.go",
    "content": "package build\n\ntype Build struct {\n\tOrganization string\n\tPipeline     string\n\tBuildNumber  int\n}\n"
  },
  {
    "path": "internal/build/resolver/cli.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/buildkite/cli/v3/internal/build\"\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n)\n\nfunc ResolveFromPositionalArgument(args []string, index int, pipeline pipelineResolver.PipelineResolverFn, conf *config.Config) BuildResolverFn {\n\treturn func(ctx context.Context) (*build.Build, error) {\n\t\t// if args does not have values, skip this resolver\n\t\tif len(args) < 1 {\n\t\t\treturn nil, nil\n\t\t}\n\t\t// if the index is out of bounds\n\t\tif (len(args) - 1) < index {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tbuild := parseBuildArg(ctx, args[index], pipeline)\n\t\t// if we get here, we should be able to parse the value and return an error if not\n\t\t// this is because a user has explicitly given an input value for us to use - we shouldnt ignore it on error\n\t\tif build == nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse the input build argument: \\\"%s\\\"\", args[index])\n\t\t}\n\n\t\treturn build, nil\n\t}\n}\n\nfunc parseBuildArg(ctx context.Context, arg string, pipeline pipelineResolver.PipelineResolverFn) *build.Build {\n\tbuildIsURL := strings.Contains(arg, \":\")\n\tbuildIsSlug := !buildIsURL && strings.Contains(arg, \"/\")\n\n\tif buildIsURL {\n\t\treturn splitBuildURL(arg)\n\t} else if buildIsSlug {\n\t\tpart := strings.Split(arg, \"/\")\n\t\tif len(part) < 3 {\n\t\t\treturn nil\n\t\t}\n\t\tnum, err := strconv.Atoi(part[2])\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &build.Build{\n\t\t\tOrganization: part[0],\n\t\t\tPipeline:     part[1],\n\t\t\tBuildNumber:  num,\n\t\t}\n\t}\n\n\tnum, err := strconv.Atoi(arg)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tp, err := pipeline(ctx)\n\tif err != nil || p == nil {\n\t\treturn nil\n\t}\n\treturn &build.Build{\n\t\tOrganization: p.Org,\n\t\tPipeline:     p.Name,\n\t\tBuildNumber:  num,\n\t}\n}\n"
  },
  {
    "path": "internal/build/resolver/cli_test.go",
    "content": "package resolver_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/build/resolver\"\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc TestParseBuildArg(t *testing.T) {\n\tt.Parallel()\n\n\ttestcases := map[string]struct {\n\t\turl, org, pipeline string\n\t\tnum                int\n\t}{\n\t\t\"org_pipeline_slug\": {\n\t\t\turl:      \"buildkite/cli/34\",\n\t\t\torg:      \"buildkite\",\n\t\t\tpipeline: \"cli\",\n\t\t\tnum:      34,\n\t\t},\n\t\t\"pipeline_slug\": {\n\t\t\turl:      \"42\",\n\t\t\torg:      \"testing\",\n\t\t\tpipeline: \"abcd\",\n\t\t\tnum:      42,\n\t\t},\n\t\t\"url\": {\n\t\t\turl:      \"https://buildkite.com/buildkite/buildkite-cli/builds/99\",\n\t\t\torg:      \"buildkite\",\n\t\t\tpipeline: \"buildkite-cli\",\n\t\t\tnum:      99,\n\t\t},\n\t}\n\n\tfor name, testcase := range testcases {\n\t\ttestcase := testcase\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\t\tconf.SelectOrganization(\"testing\", true)\n\t\t\tres := func(context.Context) (*pipeline.Pipeline, error) {\n\t\t\t\treturn &pipeline.Pipeline{\n\t\t\t\t\tName: testcase.pipeline,\n\t\t\t\t\tOrg:  testcase.org,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t\tf := resolver.ResolveFromPositionalArgument([]string{testcase.url}, 0, res, conf)\n\t\t\tbuild, err := f(context.Background())\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tif build.Organization != testcase.org {\n\t\t\t\tt.Error(\"parsed organization slug did not match expected\")\n\t\t\t}\n\t\t\tif build.Pipeline != testcase.pipeline {\n\t\t\t\tt.Error(\"parsed pipeline name did not match expected\")\n\t\t\t}\n\t\t\tif build.BuildNumber != testcase.num {\n\t\t\t\tt.Error(\"parsed build number did not match expected\")\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"Returns error if failed parsing\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\tconf.SelectOrganization(\"testing\", true)\n\t\tf := resolver.ResolveFromPositionalArgument([]string{\"https://buildkite.com/\"}, 0, nil, conf)\n\t\tbuild, err := f(context.Background())\n\t\tif err == nil {\n\t\t\tt.Error(\"should have failed parsing build\")\n\t\t}\n\t\tif build != nil {\n\t\t\tt.Error(\"no build should be returned\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/build/resolver/options/options.go",
    "content": "package options\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\tgit \"github.com/go-git/go-git/v5\"\n)\n\n// OptionsFn is a function to apply modifications to the list builds API request ie. for adding additional filters\ntype OptionsFn func(*buildkite.BuildsListOptions) error\n\ntype AggregateResolver []OptionsFn\n\nfunc (ar AggregateResolver) WithResolverWhen(condition bool, resovler OptionsFn) AggregateResolver {\n\tif condition {\n\t\treturn append(ar, resovler)\n\t}\n\treturn ar\n}\n\n// ResolveBranchFromFlag returns a function that is used to add a branch filter to a build list options\nfunc ResolveBranchFromFlag(branch string) OptionsFn {\n\treturn func(options *buildkite.BuildsListOptions) error {\n\t\tif branch != \"\" && len(options.Branch) == 0 {\n\t\t\toptions.Branch = append(options.Branch, branch)\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// ResolveBranchFromRepository returns a function that is used to add a branch filter to a build list options\nfunc ResolveBranchFromRepository(repo *git.Repository) OptionsFn {\n\treturn func(options *buildkite.BuildsListOptions) error {\n\t\tif len(options.Branch) > 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\tif repo != nil {\n\t\t\thead, err := repo.Head()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\toptions.Branch = append(options.Branch, head.Name().Short())\n\t\t\treturn nil\n\t\t}\n\n\t\tbranch, err := getBranchFromGit()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif branch != \"\" {\n\t\t\toptions.Branch = append(options.Branch, branch)\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc getBranchFromGit() (string, error) {\n\tcmd := exec.Command(\"git\", \"symbolic-ref\", \"--quiet\", \"--short\", \"HEAD\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tvar exitErr *exec.ExitError\n\t\tvar execErr *exec.Error\n\t\tif errors.As(err, &exitErr) || errors.As(err, &execErr) {\n\t\t\treturn \"\", nil\n\t\t}\n\t\treturn \"\", err\n\t}\n\n\treturn strings.TrimSpace(string(output)), nil\n}\n\n// ResolveUserFromFlag returns a function that is used to add a user filter to a build list options\nfunc ResolveUserFromFlag(user string) OptionsFn {\n\treturn func(options *buildkite.BuildsListOptions) error {\n\t\t// set the user filter if the given user exists and a filter is not already set\n\t\tif user != \"\" && options.Creator == \"\" {\n\t\t\toptions.Creator = user\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// ResolveCurrentUser returns a function that is used to add a user filter to a build list options\nfunc ResolveCurrentUser(ctx context.Context, f *factory.Factory) OptionsFn {\n\treturn func(options *buildkite.BuildsListOptions) error {\n\t\t// if creator filter already applied, dont apply another\n\t\tif options.Creator != \"\" {\n\t\t\treturn nil\n\t\t}\n\t\tuser, _, err := f.RestAPIClient.User.CurrentUser(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// set the user filter if the given user exists and a filter is not already set\n\t\toptions.Creator = user.ID\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "internal/build/resolver/options/options_test.go",
    "content": "package options\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\tgit \"github.com/go-git/go-git/v5\"\n\tgitconfig \"github.com/go-git/go-git/v5/config\"\n)\n\nfunc TestResolveBranchFromGitFallback(t *testing.T) {\n\trepo := testRepository(t, \"https://github.com/buildkite/cli.git\")\n\twt, err := repo.Worktree()\n\tif err != nil {\n\t\tt.Fatalf(\"Worktree returned error: %v\", err)\n\t}\n\troot := wt.Filesystem.Root()\n\tt.Chdir(root)\n\n\tcommitFile := filepath.Join(root, \"README.md\")\n\tif err := os.WriteFile(filepath.Join(root, \"README.md\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\tt.Fatalf(\"creating file returned error: %v\", err)\n\t}\n\tif err := exec.Command(\"git\", \"add\", filepath.Base(commitFile)).Run(); err != nil {\n\t\tt.Fatalf(\"git add returned error: %v\", err)\n\t}\n\tif err := exec.Command(\"git\", \"-c\", \"user.name=Person Example\", \"-c\", \"user.email=person@example.com\", \"-c\", \"commit.gpgsign=false\", \"commit\", \"-m\", \"initial\").Run(); err != nil {\n\t\tt.Fatalf(\"git commit returned error: %v\", err)\n\t}\n\tif err := exec.Command(\"git\", \"checkout\", \"-b\", \"feature/test\").Run(); err != nil {\n\t\tt.Fatalf(\"git checkout returned error: %v\", err)\n\t}\n\n\toptions := &buildkite.BuildsListOptions{}\n\terr = ResolveBranchFromRepository(nil)(options)\n\tif err != nil {\n\t\tt.Fatalf(\"ResolveBranchFromRepository returned error: %v\", err)\n\t}\n\tif len(options.Branch) != 1 {\n\t\tt.Fatalf(\"expected 1 branch, got %d\", len(options.Branch))\n\t}\n\tif options.Branch[0] != \"feature/test\" {\n\t\tt.Fatalf(\"expected branch feature/test, got %q\", options.Branch[0])\n\t}\n}\n\nfunc testRepository(t *testing.T, remoteURLs ...string) *git.Repository {\n\tt.Helper()\n\n\trepo, err := git.PlainInit(t.TempDir(), false)\n\tif err != nil {\n\t\tt.Fatalf(\"PlainInit returned error: %v\", err)\n\t}\n\tif len(remoteURLs) == 0 {\n\t\treturn repo\n\t}\n\n\t_, err = repo.CreateRemote(&gitconfig.RemoteConfig{Name: \"origin\", URLs: remoteURLs})\n\tif err != nil {\n\t\tt.Fatalf(\"CreateRemote returned error: %v\", err)\n\t}\n\n\treturn repo\n}\n"
  },
  {
    "path": "internal/build/resolver/resolver.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\n\t\"github.com/buildkite/cli/v3/internal/build\"\n)\n\n// BuildResolverFn is a function for finding a build. It returns an error if an irrecoverable scenario happens and\n// should halt execution. Otherwise, if the resolver does not find a build, it should return (nil, nil) to indicate\n// this. ie. no error occurred, but no build was found either\ntype BuildResolverFn func(context.Context) (*build.Build, error)\n\ntype AggregateResolver []BuildResolverFn\n\n// Resolve is a BuildResolverFn that wraps up a list of resolvers to loop through and try find a build. The first build\n// to be found will be returned. If none are found, it won't return an error to match the expectation of a\n// BuildResolverFn\n//\n// This is safe to call multiple times, the same result will be returned\nfunc (ar AggregateResolver) Resolve(ctx context.Context) (*build.Build, error) {\n\tfor _, resolve := range ar {\n\t\tb, err := resolve(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif b != nil {\n\t\t\treturn b, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\nfunc (ar AggregateResolver) WithResolverWhen(condition bool, resovler BuildResolverFn) AggregateResolver {\n\tif condition {\n\t\treturn append(ar, resovler)\n\t}\n\treturn ar\n}\n\n// NewAggregateResolver creates an AggregateResolver from a list of BuildResolverFn, appending a final resolver for\n// capturing the case that no build is found by any resolver\nfunc NewAggregateResolver(resolvers ...BuildResolverFn) AggregateResolver {\n\treturn resolvers\n}\n"
  },
  {
    "path": "internal/build/resolver/url.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\n\t\"github.com/buildkite/cli/v3/internal/build\"\n)\n\nfunc ResolveFromURL(args []string) BuildResolverFn {\n\treturn func(context.Context) (*build.Build, error) {\n\t\tif len(args) != 1 {\n\t\t\treturn nil, fmt.Errorf(\"incorrect number of arguments, expected 1, got %d\", len(args))\n\t\t}\n\t\tresolvedBuild := splitBuildURL(args[0])\n\n\t\tif resolvedBuild == nil {\n\t\t\treturn nil, fmt.Errorf(\"unable to resolve build from URL: %s\", args[0])\n\t\t}\n\n\t\treturn resolvedBuild, nil\n\t}\n}\n\nfunc splitBuildURL(url string) *build.Build {\n\tre := regexp.MustCompile(`https://buildkite.com/([^/]+)/([^/]+)/builds/(\\d+)$`)\n\tmatches := re.FindStringSubmatch(url)\n\tif matches == nil || len(matches) != 4 {\n\t\treturn nil\n\t}\n\n\tnum, err := strconv.Atoi(matches[3])\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn &build.Build{\n\t\tOrganization: matches[1],\n\t\tPipeline:     matches[2],\n\t\tBuildNumber:  num,\n\t}\n}\n"
  },
  {
    "path": "internal/build/resolver/with_options.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/buildkite/cli/v3/internal/build\"\n\t\"github.com/buildkite/cli/v3/internal/build/resolver/options\"\n\tpipelineResolver \"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc ResolveBuildWithOpts(f *factory.Factory, pipelineResolver pipelineResolver.PipelineResolverFn, listOpts ...options.OptionsFn) BuildResolverFn {\n\treturn func(ctx context.Context) (*build.Build, error) {\n\t\tpipeline, err := pipelineResolver(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif pipeline == nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to resolve a pipeline to query builds on\")\n\t\t}\n\n\t\topts := &buildkite.BuildsListOptions{\n\t\t\tListOptions: buildkite.ListOptions{\n\t\t\t\tPerPage: 1,\n\t\t\t},\n\t\t}\n\t\tfor _, opt := range listOpts {\n\t\t\terr = opt(opts)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tbuilds, _, err := f.RestAPIClient.Builds.ListByPipeline(ctx, f.Config.OrganizationSlug(), pipeline.Name, opts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(builds) == 0 {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn &build.Build{\n\t\t\tOrganization: f.Config.OrganizationSlug(),\n\t\t\tPipeline:     pipeline.Name,\n\t\t\tBuildNumber:  builds[0].Number,\n\t\t}, nil\n\t}\n}\n"
  },
  {
    "path": "internal/build/state/state.go",
    "content": "package state\n\ntype State string\n\nconst (\n\tScheduled State = \"scheduled\"\n\tRunning   State = \"running\"\n\tBlocked   State = \"blocked\"\n\tCanceling State = \"canceling\"\n\tFailing   State = \"failing\"\n\tPassed    State = \"passed\"\n\tFailed    State = \"failed\"\n\tCanceled  State = \"canceled\"\n\tSkipped   State = \"skipped\"\n\tNotRun    State = \"not_run\"\n)\n\nfunc IsTerminal(state State) bool {\n\tswitch state {\n\tcase Passed, Failed, Canceled, Skipped, NotRun:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc IsIncomplete(state State) bool {\n\tswitch state {\n\tcase Scheduled, Running, Blocked, Canceling, Failing:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "internal/build/state/state_test.go",
    "content": "package state\n\nimport \"testing\"\n\nfunc TestIsTerminal(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tstate State\n\t\twant  bool\n\t}{\n\t\t{name: \"scheduled\", state: Scheduled, want: false},\n\t\t{name: \"running\", state: Running, want: false},\n\t\t{name: \"blocked\", state: Blocked, want: false},\n\t\t{name: \"canceling\", state: Canceling, want: false},\n\t\t{name: \"failing\", state: Failing, want: false},\n\t\t{name: \"passed\", state: Passed, want: true},\n\t\t{name: \"failed\", state: Failed, want: true},\n\t\t{name: \"canceled\", state: Canceled, want: true},\n\t\t{name: \"skipped\", state: Skipped, want: true},\n\t\t{name: \"not run\", state: NotRun, want: true},\n\t\t{name: \"unknown\", state: State(\"mystery\"), want: false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := IsTerminal(tt.state); got != tt.want {\n\t\t\t\tt.Fatalf(\"IsTerminal(%q) = %v, want %v\", tt.state, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsIncomplete(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tstate State\n\t\twant  bool\n\t}{\n\t\t{name: \"scheduled\", state: Scheduled, want: true},\n\t\t{name: \"running\", state: Running, want: true},\n\t\t{name: \"blocked\", state: Blocked, want: true},\n\t\t{name: \"canceling\", state: Canceling, want: true},\n\t\t{name: \"failing\", state: Failing, want: true},\n\t\t{name: \"passed\", state: Passed, want: false},\n\t\t{name: \"failed\", state: Failed, want: false},\n\t\t{name: \"canceled\", state: Canceled, want: false},\n\t\t{name: \"skipped\", state: Skipped, want: false},\n\t\t{name: \"not run\", state: NotRun, want: false},\n\t\t{name: \"unknown\", state: State(\"mystery\"), want: false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := IsIncomplete(tt.state); got != tt.want {\n\t\t\t\tt.Fatalf(\"IsIncomplete(%q) = %v, want %v\", tt.state, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/build/view/shared/summary.go",
    "content": "package shared\n\nimport (\n\t\"github.com/buildkite/cli/v3/internal/build/view\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// BuildSummary renders a build summary that can be used by multiple commands\nfunc BuildSummary(b *buildkite.Build, organization, pipeline string) string {\n\treturn view.BuildSummary(b, organization, pipeline)\n}\n\n// BuildSummaryWithJobs renders a build summary with jobs, used by watch command\nfunc BuildSummaryWithJobs(b *buildkite.Build, organization, pipeline string) string {\n\treturn view.BuildSummaryWithJobs(b, organization, pipeline)\n}\n\n// RenderJobSummary renders a job summary that can be used by multiple commands\nfunc RenderJobSummary(j buildkite.Job) string {\n\treturn view.RenderJobSummary(j)\n}\n"
  },
  {
    "path": "internal/build/view/view.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/buildkite/cli/v3/internal/artifact\"\n\t\"github.com/buildkite/cli/v3/internal/emoji\"\n\t\"github.com/buildkite/cli/v3/internal/validation\"\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// ViewOptions represents options for viewing a build\ntype ViewOptions struct {\n\tOrganization string\n\tPipeline     string\n\tBuildNumber  int\n\tWeb          bool\n}\n\nfunc (o *ViewOptions) Validate() error {\n\tv := validation.New()\n\tv.AddRule(\"Organization\", validation.Required)\n\tv.AddRule(\"Organization\", validation.Slug)\n\tv.AddRule(\"Pipeline\", validation.Required)\n\tv.AddRule(\"Pipeline\", validation.Slug)\n\n\treturn v.Validate(map[string]interface{}{\n\t\t\"Organization\": o.Organization,\n\t\t\"Pipeline\":     o.Pipeline,\n\t})\n}\n\n// BuildView encapsulates the build view functionality\ntype BuildView struct {\n\tBuild        *buildkite.Build\n\tArtifacts    []buildkite.Artifact\n\tAnnotations  []buildkite.Annotation\n\tOrganization string\n\tPipeline     string\n}\n\n// NewBuildView creates a new BuildView instance\nfunc NewBuildView(build *buildkite.Build, artifacts []buildkite.Artifact, annotations []buildkite.Annotation, organization, pipeline string) *BuildView {\n\treturn &BuildView{\n\t\tBuild:        build,\n\t\tArtifacts:    artifacts,\n\t\tAnnotations:  annotations,\n\t\tOrganization: organization,\n\t\tPipeline:     pipeline,\n\t}\n}\n\nfunc BuildSummary(b *buildkite.Build, organization, pipeline string) string {\n\treturn buildSummary(b, organization, pipeline)\n}\n\nfunc BuildSummaryWithJobs(b *buildkite.Build, organization, pipeline string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(buildSummary(b, organization, pipeline))\n\n\tif b != nil {\n\t\tif jobs := renderJobs(b.Jobs); jobs != \"\" {\n\t\t\tsb.WriteString(\"\\n\\n\")\n\t\t\tsb.WriteString(jobs)\n\t\t}\n\t}\n\n\treturn sb.String()\n}\n\n// RenderJobSummary renders a single job's summary\nfunc RenderJobSummary(j buildkite.Job) string {\n\treturn renderJobs([]buildkite.Job{j})\n}\n\n// Render returns the complete build view\nfunc (v *BuildView) Render() string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(buildSummary(v.Build, v.Organization, v.Pipeline))\n\n\tif v.Build == nil {\n\t\treturn sb.String()\n\t}\n\n\tif jobs := renderJobs(v.Build.Jobs); jobs != \"\" {\n\t\tsb.WriteString(\"\\n\\n\")\n\t\tsb.WriteString(jobs)\n\t}\n\n\t// Add artifacts if present\n\n\tif artifacts := renderArtifacts(v.Artifacts); artifacts != \"\" {\n\t\tsb.WriteString(\"\\n\\n\")\n\t\tsb.WriteString(artifacts)\n\t}\n\n\tif annotations := renderAnnotations(v.Annotations); annotations != \"\" {\n\t\tsb.WriteString(\"\\n\\n\")\n\t\tsb.WriteString(annotations)\n\t}\n\n\treturn sb.String()\n}\n\nfunc buildSummary(b *buildkite.Build, organization, pipeline string) string {\n\tif b == nil {\n\t\treturn fmt.Sprintf(\"Build %s/%s (no data available)\\n\", output.ValueOrDash(organization), output.ValueOrDash(pipeline))\n\t}\n\n\tvar sb strings.Builder\n\n\tfmt.Fprintf(&sb, \"Build %s/%s #%d (%s)\\n\\n\", output.ValueOrDash(organization), output.ValueOrDash(pipeline), b.Number, b.State)\n\n\tsummary := output.Table(\n\t\t[]string{\"Field\", \"Value\"},\n\t\t[][]string{\n\t\t\t{\"Message\", output.ValueOrDash(truncateText(b.Message, 140))},\n\t\t\t{\"Source\", output.ValueOrDash(b.Source)},\n\t\t\t{\"Creator\", creatorName(b)},\n\t\t\t{\"Branch\", output.ValueOrDash(b.Branch)},\n\t\t\t{\"Commit\", shortenCommit(b.Commit)},\n\t\t\t{\"URL\", output.ValueOrDash(b.WebURL)},\n\t\t},\n\t\tmap[string]string{\"field\": \"bold\", \"value\": \"dim\"},\n\t)\n\n\tsb.WriteString(summary)\n\n\treturn sb.String()\n}\n\nfunc renderJobs(jobs []buildkite.Job) string {\n\tscriptJobs := filterScriptJobs(jobs)\n\tif len(scriptJobs) == 0 {\n\t\treturn \"\"\n\t}\n\n\theaders := []string{\"State\", \"Name\", \"Duration\"}\n\trows := make([][]string, 0, len(scriptJobs))\n\tfor _, job := range scriptJobs {\n\t\tname := job.Name\n\t\tif name == \"\" {\n\t\t\tname = job.Label\n\t\t}\n\t\tif name == \"\" {\n\t\t\tparts := strings.Split(job.Command, \"\\n\")\n\t\t\tif len(parts) > 0 {\n\t\t\t\tname = parts[0]\n\t\t\t}\n\t\t}\n\t\tif name == \"\" {\n\t\t\tname = \"-\"\n\t\t}\n\t\tname = truncateText(name, 72)\n\t\tname = emoji.Render(name)\n\n\t\trows = append(rows, []string{\n\t\t\tjob.State,\n\t\t\tname,\n\t\t\tformatJobDuration(job),\n\t\t})\n\t}\n\n\ttable := output.Table(headers, rows, map[string]string{\"state\": \"bold\", \"name\": \"italic\", \"duration\": \"dim\"})\n\n\treturn fmt.Sprintf(\"Jobs (%d)\\n\\n%s\", len(scriptJobs), table)\n}\n\nfunc renderArtifacts(artifacts []buildkite.Artifact) string {\n\tif len(artifacts) == 0 {\n\t\treturn \"\"\n\t}\n\n\theaders := []string{\"ID\", \"Path\", \"Size\"}\n\trows := make([][]string, 0, len(artifacts))\n\tfor _, a := range artifacts {\n\t\tsize := artifact.FormatBytes(a.FileSize)\n\t\trows = append(rows, []string{a.ID, a.Path, size})\n\t}\n\n\ttable := output.Table(headers, rows, map[string]string{\"id\": \"dim\", \"path\": \"bold\", \"size\": \"dim\"})\n\n\treturn fmt.Sprintf(\"Artifacts (%d)\\n\\n%s\", len(artifacts), table)\n}\n\nfunc renderAnnotations(annotations []buildkite.Annotation) string {\n\tif len(annotations) == 0 {\n\t\treturn \"\"\n\t}\n\n\theaders := []string{\"Style\", \"Context\"}\n\trows := make([][]string, 0, len(annotations))\n\tfor _, ann := range annotations {\n\t\trows = append(rows, []string{ann.Style, ann.Context})\n\t}\n\n\ttable := output.Table(headers, rows, map[string]string{\"style\": \"bold\", \"context\": \"italic\"})\n\n\treturn fmt.Sprintf(\"Annotations (%d)\\n\\n%s\", len(annotations), table)\n}\n\nfunc filterScriptJobs(jobs []buildkite.Job) []buildkite.Job {\n\tresult := make([]buildkite.Job, 0, len(jobs))\n\tfor _, job := range jobs {\n\t\tif job.Type == \"script\" {\n\t\t\tresult = append(result, job)\n\t\t}\n\t}\n\treturn result\n}\n\nfunc creatorName(build *buildkite.Build) string {\n\tif build == nil {\n\t\treturn \"Unknown\"\n\t}\n\tif build.Creator.ID != \"\" {\n\t\treturn build.Creator.Name\n\t}\n\tif build.Author.Username != \"\" {\n\t\treturn build.Author.Name\n\t}\n\treturn \"Unknown\"\n}\n\nfunc formatJobDuration(job buildkite.Job) string {\n\tif job.StartedAt == nil {\n\t\treturn \"-\"\n\t}\n\tif job.FinishedAt != nil {\n\t\td := job.FinishedAt.Sub(job.StartedAt.Time)\n\t\treturn formatDuration(d)\n\t}\n\treturn formatDuration(time.Since(job.StartedAt.Time)) + \" (running)\"\n}\n\nconst ellipsis = \"…\"\n\nfunc truncateText(text string, maxLength int) string {\n\trunes := []rune(text)\n\tif len(runes) <= maxLength {\n\t\treturn string(runes)\n\t}\n\treturn string(runes[:maxLength]) + ellipsis\n}\n\nfunc formatDuration(d time.Duration) string {\n\tif d == 0 {\n\t\treturn \"\"\n\t}\n\treturn d.String()\n}\n\nfunc shortenCommit(commit string) string {\n\tif strings.TrimSpace(commit) == \"\" {\n\t\treturn \"-\"\n\t}\n\tif len(commit) <= 12 {\n\t\treturn commit\n\t}\n\treturn commit[:12]\n}\n"
  },
  {
    "path": "internal/build/view/view_test.go",
    "content": "package view\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestBuildSummary_NilBuild(t *testing.T) {\n\tresult := BuildSummary(nil, \"my-org\", \"my-pipeline\")\n\n\tif !strings.Contains(result, \"my-org\") {\n\t\tt.Errorf(\"Expected result to contain organization, got: %s\", result)\n\t}\n\tif !strings.Contains(result, \"my-pipeline\") {\n\t\tt.Errorf(\"Expected result to contain pipeline, got: %s\", result)\n\t}\n\tif !strings.Contains(result, \"no data available\") {\n\t\tt.Errorf(\"Expected result to indicate no data available, got: %s\", result)\n\t}\n}\n\nfunc TestBuildSummary_ValidBuild(t *testing.T) {\n\tbuild := &buildkite.Build{\n\t\tNumber:  123,\n\t\tState:   \"passed\",\n\t\tMessage: \"Test build\",\n\t\tBranch:  \"main\",\n\t}\n\n\tresult := BuildSummary(build, \"my-org\", \"my-pipeline\")\n\n\tif !strings.Contains(result, \"#123\") {\n\t\tt.Errorf(\"Expected result to contain build number, got: %s\", result)\n\t}\n\tif !strings.Contains(result, \"passed\") {\n\t\tt.Errorf(\"Expected result to contain state, got: %s\", result)\n\t}\n}\n\nfunc TestBuildSummaryWithJobs_NilBuild(t *testing.T) {\n\tresult := BuildSummaryWithJobs(nil, \"my-org\", \"my-pipeline\")\n\n\tif !strings.Contains(result, \"my-org\") {\n\t\tt.Errorf(\"Expected result to contain organization, got: %s\", result)\n\t}\n\tif !strings.Contains(result, \"no data available\") {\n\t\tt.Errorf(\"Expected result to indicate no data available, got: %s\", result)\n\t}\n}\n\nfunc TestBuildSummaryWithJobs_ValidBuild(t *testing.T) {\n\tbuild := &buildkite.Build{\n\t\tNumber:  456,\n\t\tState:   \"running\",\n\t\tMessage: \"Test build with jobs\",\n\t\tJobs: []buildkite.Job{\n\t\t\t{Type: \"script\", Name: \"Test Job\", State: \"passed\"},\n\t\t},\n\t}\n\n\tresult := BuildSummaryWithJobs(build, \"my-org\", \"my-pipeline\")\n\n\tif !strings.Contains(result, \"#456\") {\n\t\tt.Errorf(\"Expected result to contain build number, got: %s\", result)\n\t}\n\tif !strings.Contains(result, \"Jobs\") {\n\t\tt.Errorf(\"Expected result to contain jobs section, got: %s\", result)\n\t}\n}\n\nfunc TestBuildView_Render_NilBuild(t *testing.T) {\n\tview := NewBuildView(nil, nil, nil, \"my-org\", \"my-pipeline\")\n\tresult := view.Render()\n\n\tif !strings.Contains(result, \"my-org\") {\n\t\tt.Errorf(\"Expected result to contain organization, got: %s\", result)\n\t}\n\tif !strings.Contains(result, \"no data available\") {\n\t\tt.Errorf(\"Expected result to indicate no data available, got: %s\", result)\n\t}\n}\n\nfunc TestBuildView_Render_ValidBuild(t *testing.T) {\n\tbuild := &buildkite.Build{\n\t\tNumber:  789,\n\t\tState:   \"passed\",\n\t\tMessage: \"Test build\",\n\t\tJobs: []buildkite.Job{\n\t\t\t{Type: \"script\", Name: \"Build\", State: \"passed\"},\n\t\t},\n\t}\n\tartifacts := []buildkite.Artifact{\n\t\t{ID: \"art-1\", Path: \"dist/app.js\", FileSize: 1024},\n\t}\n\tannotations := []buildkite.Annotation{\n\t\t{Style: \"info\", Context: \"test-context\"},\n\t}\n\n\tview := NewBuildView(build, artifacts, annotations, \"my-org\", \"my-pipeline\")\n\tresult := view.Render()\n\n\tif !strings.Contains(result, \"#789\") {\n\t\tt.Errorf(\"Expected result to contain build number, got: %s\", result)\n\t}\n\tif !strings.Contains(result, \"Jobs\") {\n\t\tt.Errorf(\"Expected result to contain jobs section, got: %s\", result)\n\t}\n\tif !strings.Contains(result, \"Artifacts\") {\n\t\tt.Errorf(\"Expected result to contain artifacts section, got: %s\", result)\n\t}\n\tif !strings.Contains(result, \"Annotations\") {\n\t\tt.Errorf(\"Expected result to contain annotations section, got: %s\", result)\n\t}\n}\n\nfunc TestCreatorName_NilBuild(t *testing.T) {\n\tresult := creatorName(nil)\n\n\tif result != \"Unknown\" {\n\t\tt.Errorf(\"Expected 'Unknown' for nil build, got: %s\", result)\n\t}\n}\n\nfunc TestCreatorName_WithCreator(t *testing.T) {\n\tbuild := &buildkite.Build{\n\t\tCreator: buildkite.Creator{\n\t\t\tID:   \"user-123\",\n\t\t\tName: \"John Doe\",\n\t\t},\n\t}\n\n\tresult := creatorName(build)\n\n\tif result != \"John Doe\" {\n\t\tt.Errorf(\"Expected 'John Doe', got: %s\", result)\n\t}\n}\n\nfunc TestCreatorName_WithAuthor(t *testing.T) {\n\tbuild := &buildkite.Build{\n\t\tAuthor: buildkite.Author{\n\t\t\tUsername: \"janedoe\",\n\t\t\tName:     \"Jane Doe\",\n\t\t},\n\t}\n\n\tresult := creatorName(build)\n\n\tif result != \"Jane Doe\" {\n\t\tt.Errorf(\"Expected 'Jane Doe', got: %s\", result)\n\t}\n}\n\nfunc TestCreatorName_NoCreatorOrAuthor(t *testing.T) {\n\tbuild := &buildkite.Build{}\n\n\tresult := creatorName(build)\n\n\tif result != \"Unknown\" {\n\t\tt.Errorf(\"Expected 'Unknown', got: %s\", result)\n\t}\n}\n"
  },
  {
    "path": "internal/build/watch/job.go",
    "content": "package watch\n\nimport (\n\t\"time\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// FormattedJob wraps a Buildkite job with watch-specific formatting and classification helpers.\ntype FormattedJob struct {\n\tbuildkite.Job\n}\n\n// NewFormattedJob wraps a Buildkite job.\nfunc NewFormattedJob(j buildkite.Job) FormattedJob {\n\treturn FormattedJob{Job: j}\n}\n\n// DisplayName returns a human-readable name for a job.\nfunc (j FormattedJob) DisplayName() string {\n\tif j.Name != \"\" {\n\t\treturn j.Name\n\t}\n\tif j.Label != \"\" {\n\t\treturn j.Label\n\t}\n\treturn j.Type + \" step\"\n}\n\n// Duration returns the elapsed duration for a job.\nfunc (j FormattedJob) Duration() time.Duration {\n\tif j.StartedAt == nil {\n\t\treturn 0\n\t}\n\tend := time.Now()\n\tif j.FinishedAt != nil {\n\t\tend = j.FinishedAt.Time\n\t}\n\treturn end.Sub(j.StartedAt.Time).Truncate(time.Second)\n}\n\nfunc (j FormattedJob) IsTerminalFailureState() bool {\n\treturn j.State == \"failed\" || j.State == \"timed_out\" || j.State == \"canceled\" || j.State == \"expired\"\n}\n\nfunc (j FormattedJob) IsSoftFailed() bool {\n\treturn j.SoftFailed\n}\n\nfunc (j FormattedJob) IsFailed() bool {\n\treturn j.IsTerminalFailureState()\n}\n"
  },
  {
    "path": "internal/build/watch/test_tracker.go",
    "content": "package watch\n\nimport (\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// TestTracker tracks which test executions have already been reported,\n// so that each test change is only surfaced once across polling iterations.\ntype TestTracker struct {\n\tseenExecutions map[string]bool // keyed by execution ID\n}\n\n// NewTestTracker creates a new TestTracker.\nfunc NewTestTracker() *TestTracker {\n\treturn &TestTracker{\n\t\tseenExecutions: make(map[string]bool),\n\t}\n}\n\n// Update processes a list of build tests and returns only those with\n// at least one execution that has not been seen before.\nfunc (t *TestTracker) Update(tests []buildkite.BuildTest) []buildkite.BuildTest {\n\tvar newTestChanges []buildkite.BuildTest\n\tfor _, test := range tests {\n\t\tif len(test.Executions) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\thasNewExecution := false\n\t\tfor _, execution := range test.Executions {\n\t\t\tif execution.ID != \"\" && !t.seenExecutions[execution.ID] {\n\t\t\t\tt.seenExecutions[execution.ID] = true\n\t\t\t\thasNewExecution = true\n\t\t\t}\n\t\t}\n\t\tif hasNewExecution {\n\t\t\tnewTestChanges = append(newTestChanges, test)\n\t\t}\n\n\t}\n\treturn newTestChanges\n}\n"
  },
  {
    "path": "internal/build/watch/test_tracker_test.go",
    "content": "package watch\n\nimport (\n\t\"testing\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestTestTracker_Update(t *testing.T) {\n\tt.Run(\"reports new test changes\", func(t *testing.T) {\n\t\ttracker := NewTestTracker()\n\t\ttests := []buildkite.BuildTest{\n\t\t\t{\n\t\t\t\tID:   \"test-1\",\n\t\t\t\tName: \"flaky test\",\n\t\t\t\tExecutions: []buildkite.BuildTestExecution{{\n\t\t\t\t\tID:            \"exec-1\",\n\t\t\t\t\tStatus:        \"failed\",\n\t\t\t\t\tFailureReason: \"expected 3, got 2\",\n\t\t\t\t}},\n\t\t\t},\n\t\t}\n\n\t\tnewTestChanges := tracker.Update(tests)\n\t\tif len(newTestChanges) != 1 {\n\t\t\tt.Fatalf(\"expected 1 new test change, got %d\", len(newTestChanges))\n\t\t}\n\t\tif newTestChanges[0].Name != \"flaky test\" {\n\t\t\tt.Errorf(\"expected 'flaky test', got %q\", newTestChanges[0].Name)\n\t\t}\n\t})\n\n\tt.Run(\"does not re-report same execution\", func(t *testing.T) {\n\t\ttracker := NewTestTracker()\n\t\ttests := []buildkite.BuildTest{\n\t\t\t{\n\t\t\t\tID:   \"test-1\",\n\t\t\t\tName: \"flaky test\",\n\t\t\t\tExecutions: []buildkite.BuildTestExecution{{\n\t\t\t\t\tID:            \"exec-1\",\n\t\t\t\t\tStatus:        \"failed\",\n\t\t\t\t\tFailureReason: \"expected 3, got 2\",\n\t\t\t\t}},\n\t\t\t},\n\t\t}\n\n\t\ttracker.Update(tests)\n\t\tnewTestChanges := tracker.Update(tests)\n\t\tif len(newTestChanges) != 0 {\n\t\t\tt.Errorf(\"expected 0 new test changes on second poll, got %d\", len(newTestChanges))\n\t\t}\n\t})\n\n\tt.Run(\"reports new execution for same test\", func(t *testing.T) {\n\t\ttracker := NewTestTracker()\n\t\ttracker.Update([]buildkite.BuildTest{\n\t\t\t{\n\t\t\t\tID:   \"test-1\",\n\t\t\t\tName: \"flaky test\",\n\t\t\t\tExecutions: []buildkite.BuildTestExecution{{\n\t\t\t\t\tID:            \"exec-1\",\n\t\t\t\t\tStatus:        \"failed\",\n\t\t\t\t\tFailureReason: \"first failure\",\n\t\t\t\t}},\n\t\t\t},\n\t\t})\n\n\t\tnewTestChanges := tracker.Update([]buildkite.BuildTest{\n\t\t\t{\n\t\t\t\tID:   \"test-1\",\n\t\t\t\tName: \"flaky test\",\n\t\t\t\tExecutions: []buildkite.BuildTestExecution{{\n\t\t\t\t\tID:            \"exec-2\",\n\t\t\t\t\tStatus:        \"failed\",\n\t\t\t\t\tFailureReason: \"second failure\",\n\t\t\t\t}},\n\t\t\t},\n\t\t})\n\n\t\tif len(newTestChanges) != 1 {\n\t\t\tt.Fatalf(\"expected 1 new test change, got %d\", len(newTestChanges))\n\t\t}\n\t})\n\n\tt.Run(\"skips tests without executions\", func(t *testing.T) {\n\t\ttracker := NewTestTracker()\n\t\ttests := []buildkite.BuildTest{\n\t\t\t{ID: \"test-1\", Name: \"passing test\"},\n\t\t\t{\n\t\t\t\tID:   \"test-2\",\n\t\t\t\tName: \"failing test\",\n\t\t\t\tExecutions: []buildkite.BuildTestExecution{{\n\t\t\t\t\tID:            \"exec-1\",\n\t\t\t\t\tStatus:        \"failed\",\n\t\t\t\t\tFailureReason: \"boom\",\n\t\t\t\t}},\n\t\t\t},\n\t\t}\n\n\t\tnewTestChanges := tracker.Update(tests)\n\t\tif len(newTestChanges) != 1 {\n\t\t\tt.Fatalf(\"expected 1 new test change, got %d\", len(newTestChanges))\n\t\t}\n\t\tif newTestChanges[0].ID != \"test-2\" {\n\t\t\tt.Errorf(\"expected test-2, got %s\", newTestChanges[0].ID)\n\t\t}\n\t})\n\n\tt.Run(\"skips tests with missing execution ids\", func(t *testing.T) {\n\t\ttracker := NewTestTracker()\n\t\ttests := []buildkite.BuildTest{\n\t\t\t{ID: \"test-1\", Name: \"passing test\"},\n\t\t\t{\n\t\t\t\tID:   \"test-2\",\n\t\t\t\tName: \"failing test\",\n\t\t\t\tExecutions: []buildkite.BuildTestExecution{{\n\t\t\t\t\tStatus:        \"failed\",\n\t\t\t\t\tFailureReason: \"boom\",\n\t\t\t\t}},\n\t\t\t},\n\t\t}\n\n\t\tnewTestChanges := tracker.Update(tests)\n\t\tif len(newTestChanges) != 0 {\n\t\t\tt.Fatalf(\"expected 0 new test change, got %d\", len(newTestChanges))\n\t\t}\n\t})\n\n\tt.Run(\"handles multiple new test changes at once\", func(t *testing.T) {\n\t\ttracker := NewTestTracker()\n\t\ttests := []buildkite.BuildTest{\n\t\t\t{\n\t\t\t\tID:         \"test-1\",\n\t\t\t\tExecutions: []buildkite.BuildTestExecution{{ID: \"exec-1\", Status: \"failed\"}},\n\t\t\t},\n\t\t\t{\n\t\t\t\tID:         \"test-2\",\n\t\t\t\tExecutions: []buildkite.BuildTestExecution{{ID: \"exec-2\", Status: \"failed\"}},\n\t\t\t},\n\t\t\t{\n\t\t\t\tID:         \"test-3\",\n\t\t\t\tExecutions: []buildkite.BuildTestExecution{{ID: \"exec-3\", Status: \"failed\"}},\n\t\t\t},\n\t\t}\n\n\t\tnewTestChanges := tracker.Update(tests)\n\t\tif len(newTestChanges) != 3 {\n\t\t\tt.Fatalf(\"expected 3 new test changes, got %d\", len(newTestChanges))\n\t\t}\n\t})\n\n\tt.Run(\"reports one test change when a test has multiple new executions\", func(t *testing.T) {\n\t\ttracker := NewTestTracker()\n\t\ttests := []buildkite.BuildTest{\n\t\t\t{\n\t\t\t\tID:   \"test-1\",\n\t\t\t\tName: \"flaky test\",\n\t\t\t\tExecutions: []buildkite.BuildTestExecution{\n\t\t\t\t\t{ID: \"exec-1\", Status: \"failed\", FailureReason: \"first failure\"},\n\t\t\t\t\t{ID: \"exec-2\", Status: \"failed\", FailureReason: \"second failure\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tnewTestChanges := tracker.Update(tests)\n\n\t\tif len(newTestChanges) != 1 {\n\t\t\tt.Fatalf(\"expected 1 new test change, got %d\", len(newTestChanges))\n\t\t}\n\n\t\tnewTestChanges = tracker.Update(tests)\n\t\tif len(newTestChanges) != 0 {\n\t\t\tt.Fatalf(\"expected 0 new test changes on second poll, got %d\", len(newTestChanges))\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/build/watch/tracker.go",
    "content": "package watch\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// trackedJob holds a job and its lifecycle state across polls.\ntype trackedJob struct {\n\tJob           buildkite.Job\n\tPrevState     string // state from previous poll, \"\" if first seen\n\tReported      bool   // true once surfaced to caller as failed\n\tRetryReported bool   // true once surfaced to caller as retry-passed\n}\n\n// JobSummary aggregates job counts by high-level state.\ntype JobSummary struct {\n\tPassed     int `json:\"passed\"`\n\tFailed     int `json:\"failed\"`\n\tSoftFailed int `json:\"soft_failed\"`\n\tRunning    int `json:\"running\"`\n\tScheduled  int `json:\"scheduled\"`\n\tBlocked    int `json:\"blocked\"`\n\tSkipped    int `json:\"skipped\"`\n\tWaiting    int `json:\"waiting\"`\n}\n\n// String returns a human-readable summary of non-zero job counts.\nfunc (s JobSummary) String() string {\n\ttype entry struct {\n\t\tcount int\n\t\tlabel string\n\t}\n\tentries := []entry{\n\t\t{s.Passed, \"passed\"},\n\t\t{s.Failed, \"failed\"},\n\t\t{s.SoftFailed, \"soft failed\"},\n\t\t{s.Running, \"running\"},\n\t\t{s.Scheduled, \"scheduled\"},\n\t\t{s.Blocked, \"blocked\"},\n\t\t{s.Skipped, \"skipped\"},\n\t\t{s.Waiting, \"waiting\"},\n\t}\n\tvar parts []string\n\tfor _, e := range entries {\n\t\tif e.count > 0 {\n\t\t\tparts = append(parts, fmt.Sprintf(\"%d %s\", e.count, e.label))\n\t\t}\n\t}\n\treturn strings.Join(parts, \", \")\n}\n\n// BuildStatus is the output of JobTracker.Update().\ntype BuildStatus struct {\n\tNewlyFailed      []buildkite.Job\n\tNewlyRetryPassed []buildkite.Job\n\tRunning          []buildkite.Job\n\tTotalRunning     int\n\tSummary          JobSummary\n\tBuild            buildkite.Build\n}\n\n// JobTracker tracks job state changes across polls.\ntype JobTracker struct {\n\tjobs map[string]*trackedJob\n}\n\n// NewJobTracker creates a new JobTracker.\nfunc NewJobTracker() *JobTracker {\n\treturn &JobTracker{\n\t\tjobs: make(map[string]*trackedJob),\n\t}\n}\n\n// Update processes a build and returns the current status with any state changes.\nfunc (t *JobTracker) Update(b buildkite.Build) BuildStatus {\n\tvar status BuildStatus\n\tstatus.Build = b\n\n\tvar running []buildkite.Job\n\n\tfor _, j := range b.Jobs {\n\t\tif j.Type != \"script\" || j.State == \"broken\" {\n\t\t\tcontinue\n\t\t}\n\t\tjob := NewFormattedJob(j)\n\n\t\ttj, exists := t.jobs[j.ID]\n\t\tif !exists {\n\t\t\ttj = &trackedJob{}\n\t\t\tt.jobs[j.ID] = tj\n\t\t} else {\n\t\t\ttj.PrevState = tj.Job.State\n\t\t}\n\t\ttj.Job = j\n\n\t\tprevJob := NewFormattedJob(buildkite.Job{State: tj.PrevState})\n\t\tif job.IsFailed() && !prevJob.IsTerminalFailureState() && !tj.Reported {\n\t\t\tstatus.NewlyFailed = append(status.NewlyFailed, j)\n\t\t\ttj.Reported = true\n\t\t}\n\n\t\tif isActiveState(j.State) {\n\t\t\trunning = append(running, j)\n\t\t}\n\t}\n\n\t// Second pass: detect retry jobs that just reached passed.\n\tfor _, j := range b.Jobs {\n\t\tif j.Type != \"script\" || j.State != \"passed\" || j.RetriesCount == 0 {\n\t\t\tcontinue\n\t\t}\n\t\ttj := t.jobs[j.ID]\n\t\tif tj == nil || tj.RetryReported {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, orig := range t.jobs {\n\t\t\tif orig.Job.RetriedInJobID == j.ID && orig.Reported {\n\t\t\t\tstatus.NewlyRetryPassed = append(status.NewlyRetryPassed, j)\n\t\t\t\ttj.RetryReported = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tstatus.Summary = t.summarize(b)\n\tstatus.TotalRunning = len(running)\n\tstatus.Running = running\n\n\treturn status\n}\n\n// PassedJobs returns all non-superseded jobs that passed, sorted by start time.\nfunc (t *JobTracker) PassedJobs() []buildkite.Job {\n\tvar result []buildkite.Job\n\tfor _, tj := range t.jobs {\n\t\tif tj.Job.State == \"passed\" && !tj.Job.Retried {\n\t\t\tresult = append(result, tj.Job)\n\t\t}\n\t}\n\tsortJobsByStartTime(result)\n\treturn result\n}\n\n// FailedJobs returns all hard-failed, non-superseded jobs (excludes soft failures),\n// sorted by start time.\nfunc (t *JobTracker) FailedJobs() []buildkite.Job {\n\tvar result []buildkite.Job\n\tfor _, tj := range t.jobs {\n\t\tjob := NewFormattedJob(tj.Job)\n\t\tif job.IsFailed() && !job.IsSoftFailed() && !tj.Job.Retried {\n\t\t\tresult = append(result, tj.Job)\n\t\t}\n\t}\n\tsortJobsByStartTime(result)\n\treturn result\n}\n\nfunc sortJobsByStartTime(jobs []buildkite.Job) {\n\tsort.Slice(jobs, func(i, j int) bool {\n\t\tsi, sj := jobs[i].StartedAt, jobs[j].StartedAt\n\t\tswitch {\n\t\tcase si == nil && sj == nil:\n\t\t\treturn jobs[i].ID < jobs[j].ID\n\t\tcase si == nil:\n\t\t\treturn false\n\t\tcase sj == nil:\n\t\t\treturn true\n\t\tcase si.Time.Equal(sj.Time):\n\t\t\treturn jobs[i].ID < jobs[j].ID\n\t\tdefault:\n\t\t\treturn si.Before(sj.Time)\n\t\t}\n\t})\n}\n\nfunc (t *JobTracker) summarize(b buildkite.Build) JobSummary {\n\tvar s JobSummary\n\tfor _, j := range b.Jobs {\n\t\tif j.Type != \"script\" || j.Retried {\n\t\t\tcontinue\n\t\t}\n\t\tjob := NewFormattedJob(j)\n\t\tif job.IsSoftFailed() {\n\t\t\ts.SoftFailed++\n\t\t\tcontinue\n\t\t}\n\t\tswitch j.State {\n\t\tcase \"running\", \"canceling\", \"timing_out\":\n\t\t\ts.Running++\n\t\tcase \"passed\":\n\t\t\ts.Passed++\n\t\tcase \"failed\", \"timed_out\", \"canceled\", \"expired\":\n\t\t\ts.Failed++\n\t\tcase \"skipped\", \"broken\":\n\t\t\ts.Skipped++\n\t\tcase \"blocked\", \"blocked_failed\":\n\t\t\ts.Blocked++\n\t\tcase \"scheduled\", \"assigned\", \"accepted\", \"reserved\":\n\t\t\ts.Scheduled++\n\t\tcase \"waiting\", \"waiting_failed\",\n\t\t\t\"pending\", \"limited\", \"limiting\",\n\t\t\t\"platform_limited\", \"platform_limiting\":\n\t\t\ts.Waiting++\n\t\t}\n\t}\n\treturn s\n}\n\nfunc isActiveState(state string) bool {\n\treturn state == \"running\" || state == \"canceling\" || state == \"timing_out\"\n}\n"
  },
  {
    "path": "internal/build/watch/tracker_test.go",
    "content": "package watch\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestJobTracker_Update(t *testing.T) {\n\tt.Run(\"first poll reports failures\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"passed\"},\n\t\t\t\t{ID: \"2\", Type: \"script\", State: \"failed\"},\n\t\t\t\t{ID: \"3\", Type: \"script\", State: \"running\"},\n\t\t\t},\n\t\t})\n\n\t\tif len(status.NewlyFailed) != 1 {\n\t\t\tt.Fatalf(\"expected 1 newly failed, got %d\", len(status.NewlyFailed))\n\t\t}\n\t\tif status.NewlyFailed[0].ID != \"2\" {\n\t\t\tt.Errorf(\"expected job 2, got %s\", status.NewlyFailed[0].ID)\n\t\t}\n\t})\n\n\tt.Run(\"same data second poll has no newly failed\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"failed\"},\n\t\t\t},\n\t\t})\n\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"failed\"},\n\t\t\t},\n\t\t})\n\n\t\tif len(status.NewlyFailed) != 0 {\n\t\t\tt.Errorf(\"expected 0 newly failed, got %d\", len(status.NewlyFailed))\n\t\t}\n\t})\n\n\tt.Run(\"running to failed transition\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"running\"},\n\t\t\t},\n\t\t})\n\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"failed\"},\n\t\t\t},\n\t\t})\n\n\t\tif len(status.NewlyFailed) != 1 {\n\t\t\tt.Fatalf(\"expected 1 newly failed, got %d\", len(status.NewlyFailed))\n\t\t}\n\t\tif status.NewlyFailed[0].State != \"failed\" {\n\t\t\tt.Errorf(\"expected state failed, got %s\", status.NewlyFailed[0].State)\n\t\t}\n\t})\n\n\tt.Run(\"soft failed reported\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"running\"},\n\t\t\t},\n\t\t})\n\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"failed\", SoftFailed: true},\n\t\t\t},\n\t\t})\n\n\t\tif len(status.NewlyFailed) != 1 {\n\t\t\tt.Fatalf(\"expected 1 newly failed, got %d\", len(status.NewlyFailed))\n\t\t}\n\t\tif !status.NewlyFailed[0].SoftFailed {\n\t\t\tt.Error(\"expected SoftFailed to be true\")\n\t\t}\n\t})\n\n\tt.Run(\"timed out reported as failed\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"running\"},\n\t\t\t},\n\t\t})\n\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"timed_out\"},\n\t\t\t},\n\t\t})\n\n\t\tif len(status.NewlyFailed) != 1 {\n\t\t\tt.Fatalf(\"expected 1 newly failed, got %d\", len(status.NewlyFailed))\n\t\t}\n\t\tif status.NewlyFailed[0].State != \"timed_out\" {\n\t\t\tt.Errorf(\"expected state timed_out, got %s\", status.NewlyFailed[0].State)\n\t\t}\n\t})\n\n\tt.Run(\"skips non-script and broken jobs\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"waiter\", State: \"failed\"},\n\t\t\t\t{ID: \"2\", Type: \"manual\", State: \"failed\"},\n\t\t\t\t{ID: \"3\", Type: \"script\", State: \"broken\"},\n\t\t\t\t{ID: \"4\", Type: \"script\", State: \"failed\"},\n\t\t\t},\n\t\t})\n\n\t\tif len(status.NewlyFailed) != 1 {\n\t\t\tt.Fatalf(\"expected 1 newly failed, got %d\", len(status.NewlyFailed))\n\t\t}\n\t\tif status.NewlyFailed[0].ID != \"4\" {\n\t\t\tt.Errorf(\"expected job 4, got %s\", status.NewlyFailed[0].ID)\n\t\t}\n\t})\n\n\tt.Run(\"returns all running jobs\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\tvar jobs []buildkite.Job\n\t\tfor i := 0; i < 15; i++ {\n\t\t\tjobs = append(jobs, buildkite.Job{\n\t\t\t\tID:    fmt.Sprintf(\"job-%d\", i),\n\t\t\t\tType:  \"script\",\n\t\t\t\tState: \"running\",\n\t\t\t})\n\t\t}\n\n\t\tstatus := tracker.Update(buildkite.Build{Jobs: jobs})\n\n\t\tif status.TotalRunning != 15 {\n\t\t\tt.Errorf(\"expected TotalRunning 15, got %d\", status.TotalRunning)\n\t\t}\n\t\tif len(status.Running) != 15 {\n\t\t\tt.Errorf(\"expected Running to include all 15 jobs, got %d\", len(status.Running))\n\t\t}\n\t})\n\n\tt.Run(\"new job appears mid-build\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"running\"},\n\t\t\t},\n\t\t})\n\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"running\"},\n\t\t\t\t{ID: \"2\", Type: \"script\", State: \"failed\"},\n\t\t\t},\n\t\t})\n\n\t\tif len(status.NewlyFailed) != 1 {\n\t\t\tt.Fatalf(\"expected 1 newly failed, got %d\", len(status.NewlyFailed))\n\t\t}\n\t\tif status.NewlyFailed[0].ID != \"2\" {\n\t\t\tt.Errorf(\"expected job 2, got %s\", status.NewlyFailed[0].ID)\n\t\t}\n\t})\n\n\tt.Run(\"summary counts are correct\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"passed\"},\n\t\t\t\t{ID: \"2\", Type: \"script\", State: \"failed\"},\n\t\t\t\t{ID: \"3\", Type: \"script\", State: \"running\"},\n\t\t\t\t{ID: \"4\", Type: \"script\", State: \"scheduled\"},\n\t\t\t\t{ID: \"5\", Type: \"script\", State: \"running\"},\n\t\t\t},\n\t\t})\n\n\t\tif status.Summary.Passed != 1 {\n\t\t\tt.Errorf(\"expected 1 passed, got %d\", status.Summary.Passed)\n\t\t}\n\t\tif status.Summary.Failed != 1 {\n\t\t\tt.Errorf(\"expected 1 failed, got %d\", status.Summary.Failed)\n\t\t}\n\t\tif status.Summary.Running != 2 {\n\t\t\tt.Errorf(\"expected 2 running, got %d\", status.Summary.Running)\n\t\t}\n\t\tif status.Summary.Scheduled != 1 {\n\t\t\tt.Errorf(\"expected 1 scheduled, got %d\", status.Summary.Scheduled)\n\t\t}\n\t})\n\n\tt.Run(\"failed job includes exit status and duration\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\tnow := time.Now()\n\t\tstart := now.Add(-5 * time.Second)\n\t\texitCode := 2\n\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{\n\t\t\t\t\tID:         \"1\",\n\t\t\t\t\tType:       \"script\",\n\t\t\t\t\tName:       \"lint\",\n\t\t\t\t\tState:      \"failed\",\n\t\t\t\t\tExitStatus: &exitCode,\n\t\t\t\t\tStartedAt:  &buildkite.Timestamp{Time: start},\n\t\t\t\t\tFinishedAt: &buildkite.Timestamp{Time: now},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\n\t\tif len(status.NewlyFailed) != 1 {\n\t\t\tt.Fatalf(\"expected 1 newly failed, got %d\", len(status.NewlyFailed))\n\t\t}\n\t\tfj := status.NewlyFailed[0]\n\t\tif fj.Name != \"lint\" {\n\t\t\tt.Errorf(\"expected name lint, got %s\", fj.Name)\n\t\t}\n\t\tif fj.ExitStatus == nil || *fj.ExitStatus != 2 {\n\t\t\tt.Errorf(\"expected exit status 2, got %v\", fj.ExitStatus)\n\t\t}\n\t\tif duration := NewFormattedJob(fj).Duration(); duration != 5*time.Second {\n\t\t\tt.Errorf(\"expected duration 5s, got %s\", duration)\n\t\t}\n\t})\n}\n\nfunc TestJobTracker_Update_RetriedJobs(t *testing.T) {\n\tt.Run(\"superseded job still reported as newly failed\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\t// Poll 1: job already failed and retried (e.g. automatic retry)\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\", Retried: true, RetriedInJobID: \"retry-1\"},\n\t\t\t\t{ID: \"retry-1\", Type: \"script\", State: \"running\", RetriesCount: 1},\n\t\t\t},\n\t\t})\n\t\tif len(status.NewlyFailed) != 1 {\n\t\t\tt.Fatalf(\"expected 1 newly failed (even though superseded), got %d\", len(status.NewlyFailed))\n\t\t}\n\t\tif status.NewlyFailed[0].ID != \"orig\" {\n\t\t\tt.Errorf(\"expected orig, got %s\", status.NewlyFailed[0].ID)\n\t\t}\n\n\t\t// Poll 2: same state, not re-reported\n\t\tstatus = tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\", Retried: true, RetriedInJobID: \"retry-1\"},\n\t\t\t\t{ID: \"retry-1\", Type: \"script\", State: \"running\", RetriesCount: 1},\n\t\t\t},\n\t\t})\n\t\tif len(status.NewlyFailed) != 0 {\n\t\t\tt.Errorf(\"expected 0 newly failed on re-poll, got %d\", len(status.NewlyFailed))\n\t\t}\n\t})\n\n\tt.Run(\"retry passed detected when retry job reaches passed\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\t// Poll 1: job fails\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\"},\n\t\t\t},\n\t\t})\n\n\t\t// Poll 2: original retried, retry running\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\", Retried: true, RetriedInJobID: \"retry-1\"},\n\t\t\t\t{ID: \"retry-1\", Type: \"script\", State: \"running\", RetriesCount: 1},\n\t\t\t},\n\t\t})\n\n\t\t// Poll 3: retry passes\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\", Retried: true, RetriedInJobID: \"retry-1\"},\n\t\t\t\t{ID: \"retry-1\", Type: \"script\", State: \"passed\", RetriesCount: 1},\n\t\t\t},\n\t\t})\n\t\tif len(status.NewlyRetryPassed) != 1 {\n\t\t\tt.Fatalf(\"expected 1 retry passed, got %d\", len(status.NewlyRetryPassed))\n\t\t}\n\t\tif status.NewlyRetryPassed[0].ID != \"retry-1\" {\n\t\t\tt.Errorf(\"expected retry-1, got %s\", status.NewlyRetryPassed[0].ID)\n\t\t}\n\t})\n\n\tt.Run(\"retry passed reported only once\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\"},\n\t\t\t},\n\t\t})\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\", Retried: true, RetriedInJobID: \"retry-1\"},\n\t\t\t\t{ID: \"retry-1\", Type: \"script\", State: \"passed\", RetriesCount: 1},\n\t\t\t},\n\t\t})\n\t\t// Second poll with same passed state\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\", Retried: true, RetriedInJobID: \"retry-1\"},\n\t\t\t\t{ID: \"retry-1\", Type: \"script\", State: \"passed\", RetriesCount: 1},\n\t\t\t},\n\t\t})\n\t\tif len(status.NewlyRetryPassed) != 0 {\n\t\t\tt.Errorf(\"expected 0 retry passed on second poll, got %d\", len(status.NewlyRetryPassed))\n\t\t}\n\t})\n\n\tt.Run(\"chained retries: second retry passes\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\t// Poll 1: original fails\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\"},\n\t\t\t},\n\t\t})\n\t\t// Poll 2: original retried, first retry also fails\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\", Retried: true, RetriedInJobID: \"retry-1\"},\n\t\t\t\t{ID: \"retry-1\", Type: \"script\", State: \"failed\", RetriesCount: 1},\n\t\t\t},\n\t\t})\n\t\t// Poll 3: first retry retried, second retry passes\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\", Retried: true, RetriedInJobID: \"retry-1\"},\n\t\t\t\t{ID: \"retry-1\", Type: \"script\", State: \"failed\", Retried: true, RetriedInJobID: \"retry-2\", RetriesCount: 1},\n\t\t\t\t{ID: \"retry-2\", Type: \"script\", State: \"passed\", RetriesCount: 2},\n\t\t\t},\n\t\t})\n\t\tif len(status.NewlyRetryPassed) != 1 {\n\t\t\tt.Fatalf(\"expected 1 retry passed, got %d\", len(status.NewlyRetryPassed))\n\t\t}\n\t\tif status.NewlyRetryPassed[0].ID != \"retry-2\" {\n\t\t\tt.Errorf(\"expected retry-2, got %s\", status.NewlyRetryPassed[0].ID)\n\t\t}\n\t})\n\n\tt.Run(\"summary excludes superseded jobs\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\tstatus := tracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\", Retried: true, RetriedInJobID: \"retry-1\"},\n\t\t\t\t{ID: \"retry-1\", Type: \"script\", State: \"passed\", RetriesCount: 1},\n\t\t\t},\n\t\t})\n\t\tif status.Summary.Failed != 0 {\n\t\t\tt.Errorf(\"expected 0 failed (superseded excluded), got %d\", status.Summary.Failed)\n\t\t}\n\t\tif status.Summary.Passed != 1 {\n\t\t\tt.Errorf(\"expected 1 passed, got %d\", status.Summary.Passed)\n\t\t}\n\t})\n}\n\nfunc TestJobTracker_FailedJobs(t *testing.T) {\n\tt.Run(\"returns hard failed jobs and excludes soft failures\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"failed\"},\n\t\t\t\t{ID: \"2\", Type: \"script\", State: \"running\"},\n\t\t\t},\n\t\t})\n\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"script\", State: \"failed\"},\n\t\t\t\t{ID: \"2\", Type: \"script\", State: \"failed\", SoftFailed: true},\n\t\t\t},\n\t\t})\n\n\t\tfailedJobs := tracker.FailedJobs()\n\t\tif len(failedJobs) != 1 {\n\t\t\tt.Fatalf(\"expected 1 failed job, got %d\", len(failedJobs))\n\t\t}\n\t\tif failedJobs[0].ID != \"1\" {\n\t\t\tt.Errorf(\"expected failed job 1, got %s\", failedJobs[0].ID)\n\t\t}\n\t})\n\n\tt.Run(\"excludes non-script, broken, and soft-failed jobs\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"1\", Type: \"waiter\", State: \"failed\"},\n\t\t\t\t{ID: \"2\", Type: \"script\", State: \"broken\"},\n\t\t\t\t{ID: \"3\", Type: \"script\", State: \"failed\"},\n\t\t\t\t{ID: \"4\", Type: \"script\", State: \"failed\", SoftFailed: true},\n\t\t\t},\n\t\t})\n\n\t\tfailedJobs := tracker.FailedJobs()\n\t\tif len(failedJobs) != 1 {\n\t\t\tt.Fatalf(\"expected 1 failed job, got %d\", len(failedJobs))\n\t\t}\n\t\tif failedJobs[0].ID != \"3\" {\n\t\t\tt.Errorf(\"expected failed job 3, got %s\", failedJobs[0].ID)\n\t\t}\n\t})\n\n\tt.Run(\"excludes superseded (retried) jobs\", func(t *testing.T) {\n\t\ttracker := NewJobTracker()\n\t\ttracker.Update(buildkite.Build{\n\t\t\tJobs: []buildkite.Job{\n\t\t\t\t{ID: \"orig\", Type: \"script\", State: \"failed\", Retried: true, RetriedInJobID: \"retry-1\"},\n\t\t\t\t{ID: \"still-failed\", Type: \"script\", State: \"failed\"},\n\t\t\t\t{ID: \"retry-1\", Type: \"script\", State: \"passed\", RetriesCount: 1},\n\t\t\t},\n\t\t})\n\n\t\tfailedJobs := tracker.FailedJobs()\n\t\tif len(failedJobs) != 1 {\n\t\t\tt.Fatalf(\"expected 1 failed job (superseded excluded), got %d\", len(failedJobs))\n\t\t}\n\t\tif failedJobs[0].ID != \"still-failed\" {\n\t\t\tt.Errorf(\"expected still-failed, got %s\", failedJobs[0].ID)\n\t\t}\n\t})\n}\n\nfunc TestJobTracker_PassedJobs_ExcludesSuperseded(t *testing.T) {\n\ttracker := NewJobTracker()\n\ttracker.Update(buildkite.Build{\n\t\tJobs: []buildkite.Job{\n\t\t\t{ID: \"orig\", Type: \"script\", State: \"passed\", Retried: true, RetriedInJobID: \"retry-1\"},\n\t\t\t{ID: \"retry-1\", Type: \"script\", State: \"passed\", RetriesCount: 1},\n\t\t},\n\t})\n\n\tjobs := tracker.PassedJobs()\n\tif len(jobs) != 1 {\n\t\tt.Fatalf(\"expected 1 passed job (superseded excluded), got %d\", len(jobs))\n\t}\n\tif jobs[0].ID != \"retry-1\" {\n\t\tt.Errorf(\"expected retry-1, got %s\", jobs[0].ID)\n\t}\n}\n\nfunc TestJobTracker_PassedJobs_SortedByStartTime(t *testing.T) {\n\ttracker := NewJobTracker()\n\tt1 := buildkite.Timestamp{Time: time.Date(2025, 1, 1, 0, 0, 10, 0, time.UTC)}\n\tt2 := buildkite.Timestamp{Time: time.Date(2025, 1, 1, 0, 0, 5, 0, time.UTC)}\n\ttracker.Update(buildkite.Build{\n\t\tJobs: []buildkite.Job{\n\t\t\t{ID: \"late\", Type: \"script\", State: \"passed\", StartedAt: &t1},\n\t\t\t{ID: \"early\", Type: \"script\", State: \"passed\", StartedAt: &t2},\n\t\t\t{ID: \"no-start\", Type: \"script\", State: \"passed\"},\n\t\t},\n\t})\n\n\tjobs := tracker.PassedJobs()\n\tif len(jobs) != 3 {\n\t\tt.Fatalf(\"expected 3 jobs, got %d\", len(jobs))\n\t}\n\twantOrder := []string{\"early\", \"late\", \"no-start\"}\n\tfor i, id := range wantOrder {\n\t\tif jobs[i].ID != id {\n\t\t\tt.Errorf(\"position %d: got %s, want %s\", i, jobs[i].ID, id)\n\t\t}\n\t}\n}\n\nfunc TestJobTracker_FailedJobs_SortedByStartTime(t *testing.T) {\n\ttracker := NewJobTracker()\n\tt1 := buildkite.Timestamp{Time: time.Date(2025, 1, 1, 0, 0, 20, 0, time.UTC)}\n\tt2 := buildkite.Timestamp{Time: time.Date(2025, 1, 1, 0, 0, 10, 0, time.UTC)}\n\ttracker.Update(buildkite.Build{\n\t\tJobs: []buildkite.Job{\n\t\t\t{ID: \"b\", Type: \"script\", State: \"failed\", StartedAt: &t1},\n\t\t\t{ID: \"a\", Type: \"script\", State: \"failed\", StartedAt: &t2},\n\t\t},\n\t})\n\n\tjobs := tracker.FailedJobs()\n\tif len(jobs) != 2 {\n\t\tt.Fatalf(\"expected 2 jobs, got %d\", len(jobs))\n\t}\n\tif jobs[0].ID != \"a\" || jobs[1].ID != \"b\" {\n\t\tt.Errorf(\"expected [a, b], got [%s, %s]\", jobs[0].ID, jobs[1].ID)\n\t}\n}\n\nfunc TestJobTracker_Summarize(t *testing.T) {\n\ttracker := NewJobTracker()\n\n\ttests := []struct {\n\t\tname string\n\t\tjobs []buildkite.Job\n\t\twant JobSummary\n\t}{\n\t\t{\n\t\t\tname: \"empty build\",\n\t\t\tjobs: nil,\n\t\t\twant: JobSummary{},\n\t\t},\n\t\t{\n\t\t\tname: \"skips non-script jobs\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"waiter\", State: \"passed\"},\n\t\t\t\t{Type: \"manual\", State: \"blocked\"},\n\t\t\t},\n\t\t\twant: JobSummary{},\n\t\t},\n\t\t{\n\t\t\tname: \"counts passed\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"passed\"},\n\t\t\t\t{Type: \"script\", State: \"passed\"},\n\t\t\t},\n\t\t\twant: JobSummary{Passed: 2},\n\t\t},\n\t\t{\n\t\t\tname: \"counts soft failed separately\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"failed\", SoftFailed: true},\n\t\t\t\t{Type: \"script\", State: \"passed\"},\n\t\t\t},\n\t\t\twant: JobSummary{Passed: 1, SoftFailed: 1},\n\t\t},\n\t\t{\n\t\t\tname: \"counts failed and timed_out\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"failed\"},\n\t\t\t\t{Type: \"script\", State: \"timed_out\"},\n\t\t\t},\n\t\t\twant: JobSummary{Failed: 2},\n\t\t},\n\t\t{\n\t\t\tname: \"counts running states\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"running\"},\n\t\t\t\t{Type: \"script\", State: \"canceling\"},\n\t\t\t\t{Type: \"script\", State: \"timing_out\"},\n\t\t\t},\n\t\t\twant: JobSummary{Running: 3},\n\t\t},\n\t\t{\n\t\t\tname: \"counts canceled as failed\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"canceled\"},\n\t\t\t},\n\t\t\twant: JobSummary{Failed: 1},\n\t\t},\n\t\t{\n\t\t\tname: \"counts expired as failed\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"expired\"},\n\t\t\t},\n\t\t\twant: JobSummary{Failed: 1},\n\t\t},\n\t\t{\n\t\t\tname: \"counts skipped and broken\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"skipped\"},\n\t\t\t\t{Type: \"script\", State: \"broken\"},\n\t\t\t},\n\t\t\twant: JobSummary{Skipped: 2},\n\t\t},\n\t\t{\n\t\t\tname: \"counts blocked states\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"blocked\"},\n\t\t\t\t{Type: \"script\", State: \"blocked_failed\"},\n\t\t\t},\n\t\t\twant: JobSummary{Blocked: 2},\n\t\t},\n\t\t{\n\t\t\tname: \"counts scheduled states\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"scheduled\"},\n\t\t\t\t{Type: \"script\", State: \"assigned\"},\n\t\t\t\t{Type: \"script\", State: \"accepted\"},\n\t\t\t\t{Type: \"script\", State: \"reserved\"},\n\t\t\t},\n\t\t\twant: JobSummary{Scheduled: 4},\n\t\t},\n\t\t{\n\t\t\tname: \"counts waiting states\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"waiting\"},\n\t\t\t\t{Type: \"script\", State: \"waiting_failed\"},\n\t\t\t\t{Type: \"script\", State: \"pending\"},\n\t\t\t\t{Type: \"script\", State: \"limited\"},\n\t\t\t\t{Type: \"script\", State: \"limiting\"},\n\t\t\t\t{Type: \"script\", State: \"platform_limited\"},\n\t\t\t\t{Type: \"script\", State: \"platform_limiting\"},\n\t\t\t},\n\t\t\twant: JobSummary{Waiting: 7},\n\t\t},\n\t\t{\n\t\t\tname: \"ignores unknown states\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"passed\"},\n\t\t\t\t{Type: \"script\", State: \"something_new\"},\n\t\t\t},\n\t\t\twant: JobSummary{Passed: 1},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed build\",\n\t\t\tjobs: []buildkite.Job{\n\t\t\t\t{Type: \"script\", State: \"passed\"},\n\t\t\t\t{Type: \"script\", State: \"failed\"},\n\t\t\t\t{Type: \"script\", State: \"running\"},\n\t\t\t\t{Type: \"script\", State: \"scheduled\"},\n\t\t\t\t{Type: \"waiter\", State: \"passed\"},\n\t\t\t},\n\t\t\twant: JobSummary{Passed: 1, Failed: 1, Running: 1, Scheduled: 1},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tracker.summarize(buildkite.Build{Jobs: tt.jobs})\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"summarize() = %+v, want %+v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJobSummary_String(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsummary JobSummary\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tname:    \"empty summary\",\n\t\t\tsummary: JobSummary{},\n\t\t\twant:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:    \"single field\",\n\t\t\tsummary: JobSummary{Passed: 3},\n\t\t\twant:    \"3 passed\",\n\t\t},\n\t\t{\n\t\t\tname:    \"multiple fields in order\",\n\t\t\tsummary: JobSummary{Passed: 2, Failed: 1, Running: 3},\n\t\t\twant:    \"2 passed, 1 failed, 3 running\",\n\t\t},\n\t\t{\n\t\t\tname:    \"soft failed shown separately\",\n\t\t\tsummary: JobSummary{Passed: 3, Failed: 1, SoftFailed: 2},\n\t\t\twant:    \"3 passed, 1 failed, 2 soft failed\",\n\t\t},\n\t\t{\n\t\t\tname:    \"all fields\",\n\t\t\tsummary: JobSummary{Passed: 1, Failed: 2, SoftFailed: 3, Running: 4, Scheduled: 5, Blocked: 6, Skipped: 7, Waiting: 8},\n\t\t\twant:    \"1 passed, 2 failed, 3 soft failed, 4 running, 5 scheduled, 6 blocked, 7 skipped, 8 waiting\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.summary.String()\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"String() = %q, want %q\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJob_DisplayName(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjob  buildkite.Job\n\t\twant string\n\t}{\n\t\t{\"uses Name\", buildkite.Job{Name: \"lint\"}, \"lint\"},\n\t\t{\"uses Label when no Name\", buildkite.Job{Label: \"test\"}, \"test\"},\n\t\t{\"falls back to type\", buildkite.Job{Type: \"script\"}, \"script step\"},\n\t\t{\"Name takes precedence\", buildkite.Job{Name: \"lint\", Label: \"test\"}, \"lint\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := NewFormattedJob(tt.job).DisplayName()\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"DisplayName() = %q, want %q\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJob_Duration(t *testing.T) {\n\tt.Run(\"no start time\", func(t *testing.T) {\n\t\td := NewFormattedJob(buildkite.Job{}).Duration()\n\t\tif d != 0 {\n\t\t\tt.Errorf(\"expected 0, got %s\", d)\n\t\t}\n\t})\n\n\tt.Run(\"with start and finish\", func(t *testing.T) {\n\t\tstart := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)\n\t\tfinish := time.Date(2025, 1, 1, 0, 0, 12, 0, time.UTC)\n\t\td := NewFormattedJob(buildkite.Job{\n\t\t\tStartedAt:  &buildkite.Timestamp{Time: start},\n\t\t\tFinishedAt: &buildkite.Timestamp{Time: finish},\n\t\t}).Duration()\n\t\tif d != 12*time.Second {\n\t\t\tt.Errorf(\"expected 12s, got %s\", d)\n\t\t}\n\t})\n\n\tt.Run(\"truncates to seconds\", func(t *testing.T) {\n\t\tstart := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)\n\t\tfinish := time.Date(2025, 1, 1, 0, 0, 3, 500_000_000, time.UTC)\n\t\td := NewFormattedJob(buildkite.Job{\n\t\t\tStartedAt:  &buildkite.Timestamp{Time: start},\n\t\t\tFinishedAt: &buildkite.Timestamp{Time: finish},\n\t\t}).Duration()\n\t\tif d != 3*time.Second {\n\t\t\tt.Errorf(\"expected 3s, got %s\", d)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/build/watch/watch.go",
    "content": "package watch\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\tbuildstate \"github.com/buildkite/cli/v3/internal/build/state\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nconst (\n\t// DefaultMaxConsecutiveErrors is the number of consecutive polling failures\n\t// before the watch loop aborts.\n\tDefaultMaxConsecutiveErrors = 10\n\n\t// DefaultRequestTimeout is the per-request timeout for each polling call.\n\tDefaultRequestTimeout = 30 * time.Second\n)\n\n// StatusFunc is called on each successful poll with the latest build state.\n// Returning an error aborts the watch loop and propagates that error to the caller.\ntype StatusFunc func(b buildkite.Build) error\n\n// TestStatusFunc is called with newly-seen test changes on each poll.\n// Returning an error aborts the watch loop.\ntype TestStatusFunc func(newTestChanges []buildkite.BuildTest) error\n\n// WatchOpt configures optional WatchBuild behavior.\ntype WatchOpt func(*watchConfig)\n\ntype watchConfig struct {\n\tonTestStatus       TestStatusFunc\n\tincludeRetriedJobs bool\n}\n\n// WithRetriedJobs includes retried (superseded) jobs in each poll so the\n// tracker can correlate original failures with their retry outcomes.\nfunc WithRetriedJobs() WatchOpt {\n\treturn func(c *watchConfig) {\n\t\tc.includeRetriedJobs = true\n\t}\n}\n\n// WithTestTracking enables polling BuildTests.List for failed tests on each\n// iteration, calling onTestStatus with any newly-seen test changes.\nfunc WithTestTracking(fn TestStatusFunc) WatchOpt {\n\treturn func(c *watchConfig) {\n\t\tc.onTestStatus = fn\n\t}\n}\n\n// WatchBuild polls a build until it reaches a terminal state (FinishedAt != nil).\n// It calls onStatus after each successful poll so callers can render progress.\nfunc WatchBuild(\n\tctx context.Context,\n\tclient *buildkite.Client,\n\torg, pipeline string,\n\tbuildNumber int,\n\tinterval time.Duration,\n\tonStatus StatusFunc,\n\topts ...WatchOpt,\n) (buildkite.Build, error) {\n\tcfg := &watchConfig{}\n\tfor _, opt := range opts {\n\t\topt(cfg)\n\t}\n\n\tvar testTracker *TestTracker\n\ttestPollingEnabled := false\n\tif cfg.onTestStatus != nil {\n\t\ttestTracker = NewTestTracker()\n\t\ttestPollingEnabled = true\n\t}\n\n\tvar (\n\t\tconsecutiveErrors int\n\t\tlastBuild         buildkite.Build\n\t)\n\n\tfor {\n\t\tif err := ctx.Err(); err != nil {\n\t\t\treturn lastBuild, err\n\t\t}\n\n\t\treqCtx, cancel := context.WithTimeout(ctx, DefaultRequestTimeout)\n\t\tvar getOpts *buildkite.BuildGetOptions\n\t\tif cfg.includeRetriedJobs {\n\t\t\tgetOpts = &buildkite.BuildGetOptions{\n\t\t\t\tBuildsListOptions: buildkite.BuildsListOptions{\n\t\t\t\t\tIncludeRetriedJobs: true,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\tb, _, err := client.Builds.Get(reqCtx, org, pipeline, fmt.Sprint(buildNumber), getOpts)\n\t\tcancel()\n\n\t\tif err != nil {\n\t\t\tconsecutiveErrors++\n\t\t\tif consecutiveErrors >= DefaultMaxConsecutiveErrors {\n\t\t\t\treturn lastBuild, fmt.Errorf(\"fetching build status (%d consecutive errors): %w\", consecutiveErrors, err)\n\t\t\t}\n\t\t} else {\n\t\t\tconsecutiveErrors = 0\n\t\t\tlastBuild = b\n\t\t\tif onStatus != nil {\n\t\t\t\tif err := onStatus(b); err != nil {\n\t\t\t\t\treturn b, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif testPollingEnabled && b.ID != \"\" {\n\t\t\t\tenabled, err := pollTestFailures(ctx, client, org, b.ID, testTracker, cfg.onTestStatus)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn b, err\n\t\t\t\t}\n\t\t\t\ttestPollingEnabled = enabled\n\t\t\t}\n\n\t\t\tif b.FinishedAt != nil || buildstate.IsTerminal(buildstate.State(b.State)) {\n\t\t\t\treturn b, nil\n\t\t\t}\n\t\t}\n\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn lastBuild, ctx.Err()\n\t\tcase <-time.After(interval):\n\t\t}\n\t}\n}\n\nfunc pollTestFailures(ctx context.Context, client *buildkite.Client, org, buildID string, tracker *TestTracker, onTestStatus TestStatusFunc) (bool, error) {\n\topts := &buildkite.BuildTestsListOptions{\n\t\tListOptions: buildkite.ListOptions{Page: 1, PerPage: 100},\n\t\tResult:      \"failed\",\n\t\tState:       \"enabled\",\n\t\tInclude:     \"executions\",\n\t}\n\n\tvar newTestChanges []buildkite.BuildTest\n\tfor {\n\t\treqCtx, cancel := context.WithTimeout(ctx, DefaultRequestTimeout)\n\t\ttests, resp, err := client.BuildTests.List(reqCtx, org, buildID, opts)\n\t\tcancel()\n\t\tif err != nil {\n\t\t\tif isPermanentTestPollingError(err) {\n\t\t\t\treturn false, nil\n\t\t\t}\n\n\t\t\t// Test data may not be available yet; don't treat as fatal.\n\t\t\tbreak\n\t\t}\n\n\t\tnewTestChanges = append(newTestChanges, tracker.Update(tests)...)\n\t\tif resp == nil || resp.NextPage == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\topts.Page = resp.NextPage\n\t}\n\n\tif len(newTestChanges) > 0 {\n\t\treturn true, onTestStatus(newTestChanges)\n\t}\n\n\treturn true, nil\n}\n\nfunc isPermanentTestPollingError(err error) bool {\n\tvar apiErr *buildkite.ErrorResponse\n\tif !errors.As(err, &apiErr) || apiErr.Response == nil {\n\t\treturn false\n\t}\n\n\tswitch apiErr.Response.StatusCode {\n\tcase http.StatusUnauthorized, http.StatusForbidden:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "internal/build/watch/watch_test.go",
    "content": "package watch\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestWatchBuild(t *testing.T) {\n\tt.Run(\"polls until finished\", func(t *testing.T) {\n\t\tpollCount := 0\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tif r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds/1\") {\n\t\t\t\tpollCount++\n\t\t\t\tb := buildkite.Build{Number: 1, State: \"running\"}\n\t\t\t\tif pollCount >= 3 {\n\t\t\t\t\tb.State = \"passed\"\n\t\t\t\t\tb.FinishedAt = &buildkite.Timestamp{Time: now}\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(b)\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.NotFound(w, r)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient := newTestClient(t, s.URL)\n\t\tvar statusCalls int\n\t\tb, err := WatchBuild(context.Background(), client, \"org\", \"pipe\", 1, 10*time.Millisecond, func(b buildkite.Build) error {\n\t\t\tstatusCalls++\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif b.State != \"passed\" {\n\t\t\tt.Errorf(\"expected state passed, got %s\", b.State)\n\t\t}\n\t\tif pollCount < 3 {\n\t\t\tt.Errorf(\"expected at least 3 polls, got %d\", pollCount)\n\t\t}\n\t\tif statusCalls < 3 {\n\t\t\tt.Errorf(\"expected at least 3 status calls, got %d\", statusCalls)\n\t\t}\n\t})\n\n\tt.Run(\"aborts after consecutive errors\", func(t *testing.T) {\n\t\tpollCount := 0\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tpollCount++\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient := newTestClient(t, s.URL)\n\t\t_, err := WatchBuild(context.Background(), client, \"org\", \"pipe\", 1, 10*time.Millisecond, func(b buildkite.Build) error {\n\t\t\tt.Error(\"onStatus should not be called on errors\")\n\t\t\treturn nil\n\t\t})\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error, got nil\")\n\t\t}\n\t\tif pollCount < DefaultMaxConsecutiveErrors {\n\t\t\tt.Errorf(\"expected at least %d polls, got %d\", DefaultMaxConsecutiveErrors, pollCount)\n\t\t}\n\t})\n\n\tt.Run(\"resets error count on success\", func(t *testing.T) {\n\t\tpollCount := 0\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tpollCount++\n\t\t\t// Fail for the first few, then succeed\n\t\t\tif pollCount <= 5 {\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\tNumber:     1,\n\t\t\t\tState:      \"passed\",\n\t\t\t\tFinishedAt: &buildkite.Timestamp{Time: now},\n\t\t\t})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient := newTestClient(t, s.URL)\n\t\tb, err := WatchBuild(context.Background(), client, \"org\", \"pipe\", 1, 10*time.Millisecond, func(b buildkite.Build) error { return nil })\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif b.State != \"passed\" {\n\t\t\tt.Errorf(\"expected state passed, got %s\", b.State)\n\t\t}\n\t})\n\n\tt.Run(\"returns context.DeadlineExceeded on timeout\", func(t *testing.T) {\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{Number: 1, State: \"running\"})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)\n\t\tdefer cancel()\n\n\t\tclient := newTestClient(t, s.URL)\n\t\t_, err := WatchBuild(ctx, client, \"org\", \"pipe\", 1, 10*time.Millisecond, func(b buildkite.Build) error { return nil })\n\t\tif !errors.Is(err, context.DeadlineExceeded) {\n\t\t\tt.Errorf(\"expected context.DeadlineExceeded, got: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"returns context.Canceled on explicit cancel\", func(t *testing.T) {\n\t\tpollCount := 0\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{Number: 1, State: \"running\"})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tctx, cancel := context.WithCancel(context.Background())\n\n\t\tclient := newTestClient(t, s.URL)\n\t\t_, err := WatchBuild(ctx, client, \"org\", \"pipe\", 1, 10*time.Millisecond, func(b buildkite.Build) error {\n\t\t\tpollCount++\n\t\t\tif pollCount >= 2 {\n\t\t\t\tcancel()\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Errorf(\"expected context.Canceled, got: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"nil onStatus callback\", func(t *testing.T) {\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\tNumber:     1,\n\t\t\t\tState:      \"passed\",\n\t\t\t\tFinishedAt: &buildkite.Timestamp{Time: now},\n\t\t\t})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient := newTestClient(t, s.URL)\n\t\tb, err := WatchBuild(context.Background(), client, \"org\", \"pipe\", 1, 10*time.Millisecond, nil)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif b.State != \"passed\" {\n\t\t\tt.Errorf(\"expected state passed, got %s\", b.State)\n\t\t}\n\t})\n\n\tt.Run(\"returns skipped build without finished timestamp\", func(t *testing.T) {\n\t\tpollCount := 0\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tpollCount++\n\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{\n\t\t\t\tNumber: 1,\n\t\t\t\tState:  \"skipped\",\n\t\t\t})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient := newTestClient(t, s.URL)\n\t\tb, err := WatchBuild(context.Background(), client, \"org\", \"pipe\", 1, 10*time.Millisecond, func(b buildkite.Build) error { return nil })\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif b.State != \"skipped\" {\n\t\t\tt.Errorf(\"expected state skipped, got %s\", b.State)\n\t\t}\n\t\tif pollCount != 1 {\n\t\t\tt.Errorf(\"expected 1 poll, got %d\", pollCount)\n\t\t}\n\t})\n\n\tt.Run(\"returns callback error\", func(t *testing.T) {\n\t\tcallbackErr := errors.New(\"render failed\")\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(buildkite.Build{Number: 1, State: \"running\"})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient := newTestClient(t, s.URL)\n\t\t_, err := WatchBuild(context.Background(), client, \"org\", \"pipe\", 1, 10*time.Millisecond, func(b buildkite.Build) error {\n\t\t\treturn callbackErr\n\t\t})\n\t\tif !errors.Is(err, callbackErr) {\n\t\t\tt.Fatalf(\"expected callback error, got %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"disables test polling after authorization failure\", func(t *testing.T) {\n\t\tbuildPollCount := 0\n\t\ttestPollCount := 0\n\t\tnow := time.Now()\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tswitch {\n\t\t\tcase r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds/1\"):\n\t\t\t\tbuildPollCount++\n\t\t\t\tb := buildkite.Build{ID: \"build-123\", Number: 1, State: \"running\"}\n\t\t\t\tif buildPollCount >= 3 {\n\t\t\t\t\tb.State = \"passed\"\n\t\t\t\t\tb.FinishedAt = &buildkite.Timestamp{Time: now}\n\t\t\t\t}\n\t\t\t\tjson.NewEncoder(w).Encode(b)\n\t\t\tcase r.Method == \"GET\" && strings.Contains(r.URL.Path, \"/builds/build-123/tests\"):\n\t\t\t\ttestPollCount++\n\t\t\t\tw.WriteHeader(http.StatusForbidden)\n\t\t\t\tjson.NewEncoder(w).Encode(map[string]string{\"message\": \"token is missing read_suites\"})\n\t\t\tdefault:\n\t\t\t\thttp.NotFound(w, r)\n\t\t\t}\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient := newTestClient(t, s.URL)\n\t\tb, err := WatchBuild(\n\t\t\tcontext.Background(),\n\t\t\tclient,\n\t\t\t\"org\",\n\t\t\t\"pipe\",\n\t\t\t1,\n\t\t\t10*time.Millisecond,\n\t\t\tfunc(buildkite.Build) error { return nil },\n\t\t\tWithTestTracking(func([]buildkite.BuildTest) error { return nil }),\n\t\t)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif b.State != \"passed\" {\n\t\t\tt.Fatalf(\"expected state passed, got %s\", b.State)\n\t\t}\n\t\tif buildPollCount < 3 {\n\t\t\tt.Fatalf(\"expected at least 3 build polls, got %d\", buildPollCount)\n\t\t}\n\t\tif testPollCount != 1 {\n\t\t\tt.Fatalf(\"expected test polling to stop after auth failure, got %d requests\", testPollCount)\n\t\t}\n\t})\n}\n\nfunc TestPollTestFailures(t *testing.T) {\n\tt.Run(\"follows pagination\", func(t *testing.T) {\n\t\tvar requestedPages []string\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\t\tif r.Method != \"GET\" || !strings.Contains(r.URL.Path, \"/builds/build-123/tests\") {\n\t\t\t\thttp.NotFound(w, r)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequestedPages = append(requestedPages, r.URL.Query().Get(\"page\"))\n\t\t\tif got, want := r.URL.Query().Get(\"include\"), \"executions\"; got != want {\n\t\t\t\tt.Fatalf(\"include = %q, want %q\", got, want)\n\t\t\t}\n\t\t\tswitch r.URL.Query().Get(\"page\") {\n\t\t\tcase \"1\":\n\t\t\t\tw.Header().Set(\"Link\", \"</v2/analytics/organizations/org/builds/build-123/tests?page=2&per_page=10>; rel=\\\"next\\\"\")\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.BuildTest{\n\t\t\t\t\t{ID: \"test-1\", Name: \"first-page failure\", Executions: []buildkite.BuildTestExecution{{ID: \"exec-1\", Status: \"failed\"}}},\n\t\t\t\t})\n\t\t\tcase \"2\":\n\t\t\t\tjson.NewEncoder(w).Encode([]buildkite.BuildTest{\n\t\t\t\t\t{ID: \"test-2\", Name: \"second-page failure\", Executions: []buildkite.BuildTestExecution{{ID: \"exec-2\", Status: \"failed\"}}},\n\t\t\t\t})\n\t\t\tdefault:\n\t\t\t\tt.Fatalf(\"unexpected page %q\", r.URL.Query().Get(\"page\"))\n\t\t\t}\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient := newTestClient(t, s.URL)\n\t\tvar reported []buildkite.BuildTest\n\t\tenabled, err := pollTestFailures(context.Background(), client, \"org\", \"build-123\", NewTestTracker(), func(newTestChanges []buildkite.BuildTest) error {\n\t\t\treported = append(reported, newTestChanges...)\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif !enabled {\n\t\t\tt.Fatal(\"expected test polling to remain enabled\")\n\t\t}\n\n\t\tif got, want := requestedPages, []string{\"1\", \"2\"}; len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {\n\t\t\tt.Fatalf(\"requested pages = %v, want %v\", got, want)\n\t\t}\n\t\tif got, want := len(reported), 2; got != want {\n\t\t\tt.Fatalf(\"reported %d test changes, want %d\", got, want)\n\t\t}\n\t\tif got, want := reported[1].Name, \"second-page failure\"; got != want {\n\t\t\tt.Fatalf(\"reported second page failure %q, want %q\", got, want)\n\t\t}\n\t})\n\n\tt.Run(\"disables polling on authorization errors\", func(t *testing.T) {\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tw.WriteHeader(http.StatusForbidden)\n\t\t\tjson.NewEncoder(w).Encode(map[string]string{\"message\": \"token is missing read_suites\"})\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tclient := newTestClient(t, s.URL)\n\t\tenabled, err := pollTestFailures(context.Background(), client, \"org\", \"build-123\", NewTestTracker(), func([]buildkite.BuildTest) error {\n\t\t\tt.Fatal(\"onTestStatus should not be called on authorization errors\")\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif enabled {\n\t\t\tt.Fatal(\"expected test polling to be disabled\")\n\t\t}\n\t})\n}\n\nfunc newTestClient(t *testing.T, baseURL string) *buildkite.Client {\n\tt.Helper()\n\tclient, err := buildkite.NewOpts(\n\t\tbuildkite.WithBaseURL(baseURL),\n\t\tbuildkite.WithTokenAuth(\"test-token\"),\n\t)\n\tif err != nil {\n\t\tt.Fatalf(\"creating test client: %v\", err)\n\t}\n\treturn client\n}\n"
  },
  {
    "path": "internal/cli/context.go",
    "content": "package cli\n\ntype GlobalFlags interface {\n\tSkipConfirmation() bool\n\tDisableInput() bool\n\tIsQuiet() bool\n\tDisablePager() bool\n\tEnableDebug() bool\n}\n\ntype Globals struct {\n\tYes     bool\n\tNoInput bool\n\tQuiet   bool\n\tNoPager bool\n\tDebug   bool\n}\n\nfunc (g Globals) SkipConfirmation() bool {\n\treturn g.Yes\n}\n\nfunc (g Globals) DisableInput() bool {\n\treturn g.NoInput\n}\n\nfunc (g Globals) IsQuiet() bool {\n\treturn g.Quiet\n}\n\nfunc (g Globals) DisablePager() bool {\n\treturn g.NoPager\n}\n\nfunc (g Globals) EnableDebug() bool {\n\treturn g.Debug\n}\n"
  },
  {
    "path": "internal/cluster/list_queues.graphql",
    "content": "query GetClusterQueues($orgSlug: ID!, $clusterId: ID!) {\n  organization(slug: $orgSlug) {\n    cluster(id: $clusterId) {\n      name\n      description\n      queues(first: 10) {\n        edges {\n          node {\n            id\n            uuid\n            key\n            description\n          }\n        }\n      }\n    }\n  }\n}\n\nquery GetClusterQueueAgent($orgSlug: ID!, $queueId: [ID!]) {\n  organization(slug: $orgSlug) {\n    agents(first: 10, clusterQueue: $queueId) {\n      edges {\n        node {\n          name\n          hostname\n          version\n          id\n          clusterQueue{\n            id\n            uuid\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "internal/cluster/query.go",
    "content": "package cluster\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/buildkite/cli/v3/internal/graphql\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc GetQueues(ctx context.Context, f *factory.Factory, orgSlug string, clusterID string, lo *buildkite.ClusterQueuesListOptions) ([]buildkite.ClusterQueue, error) {\n\tqueues, _, err := f.RestAPIClient.ClusterQueues.List(ctx, orgSlug, clusterID, lo)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tqueuesResponse := make([]buildkite.ClusterQueue, len(queues))\n\tvar wg sync.WaitGroup\n\terrChan := make(chan error, len(queues))\n\tfor i, q := range queues {\n\t\twg.Add(1)\n\t\tgo func(i int, q buildkite.ClusterQueue) {\n\t\t\tdefer wg.Done()\n\t\t\tqueuesResponse[i] = buildkite.ClusterQueue{\n\t\t\t\tCreatedAt:          q.CreatedAt,\n\t\t\t\tCreatedBy:          q.CreatedBy,\n\t\t\t\tDescription:        q.Description,\n\t\t\t\tDispatchPaused:     q.DispatchPaused,\n\t\t\t\tDispatchPausedAt:   q.DispatchPausedAt,\n\t\t\t\tDispatchPausedBy:   q.DispatchPausedBy,\n\t\t\t\tDispatchPausedNote: q.DispatchPausedNote,\n\t\t\t\tID:                 q.ID,\n\t\t\t\tKey:                q.Key,\n\t\t\t\tURL:                q.URL,\n\t\t\t\tWebURL:             q.WebURL,\n\t\t\t}\n\t\t}(i, q)\n\t}\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(errChan)\n\t}()\n\n\tfor err := range errChan {\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn queuesResponse, nil\n}\n\nfunc GetQueueAgentCount(ctx context.Context, f *factory.Factory, orgSlug string, queues ...buildkite.ClusterQueue) (int, error) {\n\tqueueIDs := []string{}\n\tfor _, q := range queues {\n\t\tqueueIDs = append(queueIDs, q.ID)\n\t}\n\tagent, err := graphql.GetClusterQueueAgent(ctx, f.GraphQLClient, orgSlug, queueIDs)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn len(agent.Organization.Agents.Edges), nil\n}\n"
  },
  {
    "path": "internal/cluster/view.go",
    "content": "package cluster\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// ClusterViewTable renders a table view of one or more clusters\nfunc ClusterViewTable(c ...buildkite.Cluster) string {\n\tif len(c) == 0 {\n\t\treturn \"No clusters found.\"\n\t}\n\n\tif len(c) == 1 {\n\t\treturn renderSingleClusterDetail(c[0])\n\t}\n\n\trows := make([][]string, 0, len(c))\n\tfor _, cluster := range c {\n\t\trows = append(rows, []string{\n\t\t\toutput.ValueOrDash(cluster.Name),\n\t\t\toutput.ValueOrDash(cluster.ID),\n\t\t\toutput.ValueOrDash(cluster.DefaultQueueID),\n\t\t})\n\t}\n\n\treturn output.Table(\n\t\t[]string{\"Name\", \"ID\", \"Default Queue ID\"},\n\t\trows,\n\t\tmap[string]string{\"name\": \"bold\", \"id\": \"dim\", \"default queue id\": \"dim\"},\n\t)\n}\n\nfunc renderSingleClusterDetail(c buildkite.Cluster) string {\n\tvar sb strings.Builder\n\tfmt.Fprintf(&sb, \"Viewing %s\\n\\n\", output.ValueOrDash(c.Name))\n\n\trows := [][]string{\n\t\t{\"Description\", output.ValueOrDash(c.Description)},\n\t\t{\"Color\", output.ValueOrDash(c.Color)},\n\t\t{\"Emoji\", output.ValueOrDash(c.Emoji)},\n\t\t{\"ID\", output.ValueOrDash(c.ID)},\n\t\t{\"GraphQL ID\", output.ValueOrDash(c.GraphQLID)},\n\t\t{\"Default Queue ID\", output.ValueOrDash(c.DefaultQueueID)},\n\t\t{\"Web URL\", output.ValueOrDash(c.WebURL)},\n\t\t{\"API URL\", output.ValueOrDash(c.URL)},\n\t\t{\"Queues URL\", output.ValueOrDash(c.QueuesURL)},\n\t\t{\"Queue URL\", output.ValueOrDash(c.DefaultQueueURL)},\n\t}\n\n\tif c.CreatedBy.ID != \"\" {\n\t\trows = append(\n\t\t\trows,\n\t\t\t[]string{\"Created By Name\", output.ValueOrDash(c.CreatedBy.Name)},\n\t\t\t[]string{\"Created By Email\", output.ValueOrDash(c.CreatedBy.Email)},\n\t\t\t[]string{\"Created By ID\", output.ValueOrDash(c.CreatedBy.ID)},\n\t\t)\n\t}\n\n\tif c.CreatedAt != nil {\n\t\trows = append(rows, []string{\"Created At\", c.CreatedAt.Format(time.RFC3339)})\n\t}\n\n\ttable := output.Table(\n\t\t[]string{\"Field\", \"Value\"},\n\t\trows,\n\t\tmap[string]string{\"field\": \"dim\", \"value\": \"italic\"},\n\t)\n\n\tsb.WriteString(table)\n\treturn sb.String()\n}\n"
  },
  {
    "path": "internal/config/config.go",
    "content": "// Package config contains the configuration for the bk CLI\n//\n// Configuration can come from files or environment variables. File based configuration works similar to unix config\n// file hierarchy where there is a \"user\" config file found under $HOME, and also a local config in the current\n// repository root (referred to as \"local\" config)\npackage config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n\t\"github.com/buildkite/cli/v3/pkg/keyring\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\tgit \"github.com/go-git/go-git/v5\"\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/spf13/afero\"\n)\n\nvar (\n\tlegacyTokenWarningOnce sync.Once\n\tenvTokenWarningOnce    sync.Once\n)\n\nconst (\n\tDefaultGraphQLEndpoint = \"https://graphql.buildkite.com/v1\"\n\n\t// ExperimentPreflight is the experiment flag name for the preflight command.\n\tExperimentPreflight = \"preflight\"\n\t// DefaultExperiments is the comma-separated experiment list enabled out-of-the-box.\n\tDefaultExperiments = ExperimentPreflight\n\n\tappData             = \"AppData\"\n\tconfigFilePath      = \"bk.yaml\"\n\tlocalConfigFilePath = \".\" + configFilePath\n\txdgConfigHome       = \"XDG_CONFIG_HOME\"\n)\n\ntype orgConfig struct {\n\tAPIToken string `yaml:\"api_token,omitempty\"`\n}\n\ntype fileConfig struct {\n\tSelectedOrg   string               `yaml:\"selected_org\"`\n\tOrganizations map[string]orgConfig `yaml:\"organizations,omitempty\"`\n\tPipelines     []string             `yaml:\"pipelines,omitempty\"`\n\tNoPager       bool                 `yaml:\"no_pager,omitempty\"`\n\tOutputFormat  string               `yaml:\"output_format,omitempty\"`\n\tQuiet         bool                 `yaml:\"quiet,omitempty\"`\n\tNoInput       bool                 `yaml:\"no_input,omitempty\"`\n\tPager         string               `yaml:\"pager,omitempty\"`\n\tTelemetry     *bool                `yaml:\"telemetry,omitempty\"`\n\tExperiments   string               `yaml:\"experiments,omitempty\"`\n}\n\n// Config contains the configuration for the currently selected organization\n// to operate on with the CLI application\ntype Config struct {\n\tfs        afero.Fs\n\tuserPath  string\n\tlocalPath string\n\n\tuser  fileConfig\n\tlocal fileConfig\n}\n\nfunc New(fs afero.Fs, repo *git.Repository) *Config {\n\tif fs == nil {\n\t\tfs = afero.NewOsFs()\n\t}\n\n\tuserPath := configFile()\n\tlocalPath := localConfigFilePath\n\tif repo != nil {\n\t\tif wt, _ := repo.Worktree(); wt != nil {\n\t\t\tlocalPath = filepath.Join(wt.Filesystem.Root(), localConfigFilePath)\n\t\t}\n\t}\n\n\tuserCfg, userErr := loadFileConfig(fs, userPath)\n\tif userErr != nil {\n\t\tfmt.Fprintf(os.Stderr, \"warning: failed to read config %s: %v\\n\", userPath, userErr)\n\t}\n\n\tlocalCfg, localErr := loadFileConfig(fs, localPath)\n\tif localErr != nil {\n\t\tfmt.Fprintf(os.Stderr, \"warning: failed to read config %s: %v\\n\", localPath, localErr)\n\t}\n\n\treturn &Config{\n\t\tfs:        fs,\n\t\tuserPath:  userPath,\n\t\tlocalPath: localPath,\n\t\tuser:      userCfg,\n\t\tlocal:     localCfg,\n\t}\n}\n\n// OrganizationSlug gets the slug for the currently selected organization. This can be configured locally or per user.\n// This will search for configuration in that order.\nfunc (conf *Config) OrganizationSlug() string {\n\treturn firstNonEmpty(\n\t\tos.Getenv(\"BUILDKITE_ORGANIZATION_SLUG\"),\n\t\tconf.local.SelectedOrg,\n\t\tconf.user.SelectedOrg,\n\t)\n}\n\n// SelectOrganization sets the selected organization in the configuration file\nfunc (conf *Config) SelectOrganization(org string, inGitRepo bool) error {\n\tif !inGitRepo {\n\t\tconf.user.SelectedOrg = org\n\t\treturn conf.writeUser()\n\t}\n\n\tconf.local.SelectedOrg = org\n\treturn conf.writeLocal()\n}\n\n// APIToken gets the API token configured for the currently selected organization.\n// Precedence: environment variable > keyring > config file (legacy, read-only with warning)\nfunc (conf *Config) APIToken() string {\n\treturn conf.APITokenForOrg(conf.OrganizationSlug())\n}\n\n// APITokenForOrg gets the API token for a specific organization.\n// Precedence: environment variable > keyring > config file (legacy, read-only with warning)\nfunc (conf *Config) APITokenForOrg(org string) string {\n\tif token := os.Getenv(\"BUILDKITE_API_TOKEN\"); token != \"\" {\n\t\tenvTokenWarningOnce.Do(func() {\n\t\t\tfmt.Fprintln(os.Stderr, \"Warning: using BUILDKITE_API_TOKEN environment variable for authentication.\")\n\t\t})\n\t\treturn token\n\t}\n\n\tkr := keyring.New()\n\tif kr.IsAvailable() {\n\t\tif token, err := kr.Get(org); err == nil && token != \"\" {\n\t\t\treturn token\n\t\t}\n\t}\n\n\t// Legacy fallback: read tokens from config files (read-only)\n\tif token := firstNonEmpty(\n\t\tconf.user.getToken(org),\n\t\tconf.local.getToken(org),\n\t); token != \"\" {\n\t\tlegacyTokenWarningOnce.Do(func() {\n\t\t\tfmt.Fprintln(os.Stderr, \"Warning: reading API token from config file is deprecated. Run `bk auth login` to store your token securely in the system keychain.\")\n\t\t})\n\t\treturn token\n\t}\n\n\treturn \"\"\n}\n\n// RefreshTokenForOrg gets the refresh token for a specific organization from the keyring.\nfunc (conf *Config) RefreshTokenForOrg(org string) string {\n\tif org == \"\" {\n\t\treturn \"\"\n\t}\n\tkr := keyring.New()\n\tif kr.IsAvailable() {\n\t\tif token, err := kr.GetRefreshToken(org); err == nil && token != \"\" {\n\t\t\treturn token\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// RefreshToken gets the refresh token for the currently selected organization.\nfunc (conf *Config) RefreshToken() string {\n\treturn conf.RefreshTokenForOrg(conf.OrganizationSlug())\n}\n\n// HasStoredTokenForOrg reports whether a token is stored for org in keyring\n// or config files, excluding environment variable overrides.\nfunc (conf *Config) HasStoredTokenForOrg(org string) bool {\n\tif org == \"\" {\n\t\treturn false\n\t}\n\n\tkr := keyring.New()\n\tif kr.IsAvailable() {\n\t\tif token, err := kr.Get(org); err == nil && token != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// Legacy fallback: check config files (read-only)\n\treturn firstNonEmpty(\n\t\tconf.user.getToken(org),\n\t\tconf.local.getToken(org),\n\t) != \"\"\n}\n\n// EnsureOrganization records an organization in user config without requiring\n// a token value. This keeps org switching/listing functional for keychain-only\n// token storage.\nfunc (conf *Config) EnsureOrganization(org string) error {\n\tif org == \"\" {\n\t\treturn nil\n\t}\n\tif conf.user.Organizations == nil {\n\t\tconf.user.Organizations = make(map[string]orgConfig)\n\t}\n\tif _, exists := conf.user.Organizations[org]; exists {\n\t\treturn nil\n\t}\n\tconf.user.Organizations[org] = orgConfig{}\n\treturn conf.writeUser()\n}\n\nfunc (conf *Config) ConfiguredOrganizations() []string {\n\torgs := slices.Collect(maps.Keys(conf.user.Organizations))\n\tif o := os.Getenv(\"BUILDKITE_ORGANIZATION_SLUG\"); o != \"\" {\n\t\torgs = append(orgs, o)\n\t}\n\treturn orgs\n}\n\nfunc (conf *Config) GetGraphQLEndpoint() string {\n\tvalue := os.Getenv(\"BUILDKITE_GRAPHQL_ENDPOINT\")\n\tif value != \"\" {\n\t\treturn value\n\t}\n\treturn DefaultGraphQLEndpoint\n}\n\nfunc (conf *Config) RESTAPIEndpoint() string {\n\tvalue := os.Getenv(\"BUILDKITE_REST_API_ENDPOINT\")\n\tif value != \"\" {\n\t\treturn value\n\t}\n\n\treturn buildkite.DefaultBaseURL\n}\n\nfunc (conf *Config) PagerDisabled() bool {\n\tif v, ok := lookupBoolEnv(\"BUILDKITE_NO_PAGER\"); ok {\n\t\treturn v\n\t}\n\tif v, ok := lookupBoolEnv(\"NO_PAGER\"); ok {\n\t\treturn v\n\t}\n\n\tif conf.local.NoPager {\n\t\treturn true\n\t}\n\n\treturn conf.user.NoPager\n}\n\nfunc (conf *Config) SetNoPager(v bool, saveLocal bool) error {\n\tif !saveLocal {\n\t\tconf.user.NoPager = v\n\t\treturn conf.writeUser()\n\t}\n\tconf.local.NoPager = v\n\treturn conf.writeLocal()\n}\n\n// OutputFormat returns the configured output format (json, yaml, text).\n// Precedence: env > local > user > default (json)\nfunc (conf *Config) OutputFormat() string {\n\treturn firstNonEmpty(\n\t\tos.Getenv(\"BUILDKITE_OUTPUT_FORMAT\"),\n\t\tconf.local.OutputFormat,\n\t\tconf.user.OutputFormat,\n\t\t\"json\",\n\t)\n}\n\nfunc (conf *Config) SetOutputFormat(v string, saveLocal bool) error {\n\tif !saveLocal {\n\t\tconf.user.OutputFormat = v\n\t\treturn conf.writeUser()\n\t}\n\tconf.local.OutputFormat = v\n\treturn conf.writeLocal()\n}\n\n// Quiet returns whether quiet mode is enabled.\n// Precedence: env > local > user\nfunc (conf *Config) Quiet() bool {\n\tif v, ok := lookupBoolEnv(\"BUILDKITE_QUIET\"); ok {\n\t\treturn v\n\t}\n\n\tif conf.local.Quiet {\n\t\treturn true\n\t}\n\n\treturn conf.user.Quiet\n}\n\nfunc (conf *Config) SetQuiet(v bool, saveLocal bool) error {\n\tif !saveLocal {\n\t\tconf.user.Quiet = v\n\t\treturn conf.writeUser()\n\t}\n\tconf.local.Quiet = v\n\treturn conf.writeLocal()\n}\n\n// NoInput returns whether interactive input is disabled.\n// Precedence: env > user (not stored in local config)\nfunc (conf *Config) NoInput() bool {\n\tif v, ok := lookupBoolEnv(\"BUILDKITE_NO_INPUT\"); ok {\n\t\treturn v\n\t}\n\n\treturn conf.user.NoInput\n}\n\n// SetNoInput sets whether interactive input is disabled (user config only)\nfunc (conf *Config) SetNoInput(v bool) error {\n\tconf.user.NoInput = v\n\treturn conf.writeUser()\n}\n\n// Pager returns the configured pager command.\n// Precedence: PAGER env > user config > default (less -R)\nfunc (conf *Config) Pager() string {\n\treturn firstNonEmpty(\n\t\tos.Getenv(\"PAGER\"),\n\t\tconf.user.Pager,\n\t\t\"less -R\",\n\t)\n}\n\n// SetPager sets the pager command (user config only)\nfunc (conf *Config) SetPager(v string) error {\n\tconf.user.Pager = v\n\treturn conf.writeUser()\n}\n\n// TelemetryEnabled returns whether telemetry is enabled.\n// Defaults to true if not explicitly set.\n// Precedence: env > user config\nfunc (conf *Config) TelemetryEnabled() bool {\n\tif v, ok := lookupBoolEnv(\"BK_TELEMETRY\"); ok {\n\t\treturn v\n\t}\n\n\tif conf.user.Telemetry != nil {\n\t\treturn *conf.user.Telemetry\n\t}\n\n\treturn true\n}\n\n// SetTelemetry sets whether telemetry is enabled (user config only)\nfunc (conf *Config) SetTelemetry(v bool) error {\n\tconf.user.Telemetry = &v\n\treturn conf.writeUser()\n}\n\n// Experiments returns the comma-separated list of enabled experiments.\n// Precedence: env (even if empty) > user config > default\nfunc (conf *Config) Experiments() string {\n\tif v, ok := os.LookupEnv(\"BUILDKITE_EXPERIMENTS\"); ok {\n\t\treturn v\n\t}\n\tif conf.user.Experiments != \"\" {\n\t\treturn conf.user.Experiments\n\t}\n\treturn DefaultExperiments\n}\n\n// HasExperiment reports whether the given experiment name is enabled.\nfunc (conf *Config) HasExperiment(name string) bool {\n\tfor _, exp := range strings.Split(conf.Experiments(), \",\") {\n\t\tif exp := strings.TrimSpace(exp); exp != \"\" && exp == name {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// SetExperiments sets the experiments string (user config only)\nfunc (conf *Config) SetExperiments(v string) error {\n\tconf.user.Experiments = v\n\treturn conf.writeUser()\n}\n\nfunc lookupBoolEnv(key string) (bool, bool) {\n\tv := os.Getenv(key)\n\tif v == \"\" {\n\t\treturn false, false\n\t}\n\tb, err := strconv.ParseBool(v)\n\tif err != nil {\n\t\treturn false, false\n\t}\n\treturn b, true\n}\n\n// ClearAllOrganizations removes all organization entries and the selected\n// organization from the user configuration file.\nfunc (conf *Config) ClearAllOrganizations() error {\n\tconf.user.Organizations = make(map[string]orgConfig)\n\tconf.user.SelectedOrg = \"\"\n\treturn conf.writeUser()\n}\n\nfunc (conf *Config) HasConfiguredOrganization(slug string) bool {\n\treturn slices.Contains(conf.ConfiguredOrganizations(), slug)\n}\n\n// PreferredPipelines will retrieve the list of pipelines from local configuration\nfunc (conf *Config) PreferredPipelines() []pipeline.Pipeline {\n\tnames := conf.local.Pipelines\n\n\tif len(names) == 0 {\n\t\treturn []pipeline.Pipeline{}\n\t}\n\n\tpipelines := make([]pipeline.Pipeline, len(names))\n\tfor i, v := range names {\n\t\tpipelines[i] = pipeline.Pipeline{\n\t\t\tName: v,\n\t\t\tOrg:  conf.OrganizationSlug(),\n\t\t}\n\t}\n\n\treturn pipelines\n}\n\n// SetPreferredPipelines will write the provided list of pipelines to local configuration\nfunc (conf *Config) SetPreferredPipelines(pipelines []pipeline.Pipeline) error {\n\t// only save pipelines if they are present\n\tif len(pipelines) == 0 {\n\t\treturn nil\n\t}\n\n\tnames := make([]string, len(pipelines))\n\tfor i, p := range pipelines {\n\t\tnames[i] = p.Name\n\t}\n\tconf.local.Pipelines = names\n\treturn conf.writeLocal()\n}\n\nfunc firstNonEmpty(s ...string) string {\n\tfor _, k := range s {\n\t\tif k != \"\" {\n\t\t\treturn k\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// Config path precedence: XDG_CONFIG_HOME, AppData (windows only), HOME.\nfunc configFile() string {\n\tvar path string\n\tif a := os.Getenv(xdgConfigHome); a != \"\" {\n\t\tpath = filepath.Join(a, configFilePath)\n\t} else if b := os.Getenv(appData); runtime.GOOS == \"windows\" && b != \"\" {\n\t\tpath = filepath.Join(b, \"Buildkite CLI\", configFilePath)\n\t} else {\n\t\tc, err := createIfNotExistsConfigDir()\n\t\tif err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t\tpath = filepath.Join(c, configFilePath)\n\t}\n\treturn path\n}\n\nfunc createIfNotExistsConfigDir() (string, error) {\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tconfigDir := filepath.Join(homeDir, \".config\")\n\tif _, err := os.Stat(configDir); errors.Is(err, os.ErrNotExist) {\n\t\terr := os.Mkdir(configDir, 0o755)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t} else if err != nil {\n\t\t// Other error occurred in checking the dir\n\t\treturn \"\", err\n\t}\n\treturn configDir, nil\n}\n\nfunc loadFileConfig(fs afero.Fs, path string) (fileConfig, error) {\n\tcfg := fileConfig{Organizations: make(map[string]orgConfig)}\n\tif path == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tfile, err := fs.Open(path)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn cfg, nil\n\t\t}\n\t\treturn cfg, err\n\t}\n\tdefer file.Close()\n\n\tcontent, err := io.ReadAll(file)\n\tif err != nil {\n\t\treturn cfg, err\n\t}\n\tif len(content) == 0 {\n\t\treturn cfg, nil\n\t}\n\n\tif err := yaml.Unmarshal(content, &cfg); err != nil {\n\t\treturn cfg, err\n\t}\n\tif cfg.Organizations == nil {\n\t\tcfg.Organizations = make(map[string]orgConfig)\n\t}\n\treturn cfg, nil\n}\n\nfunc writeFileConfig(fs afero.Fs, path string, cfg fileConfig) error {\n\tif path == \"\" {\n\t\treturn nil\n\t}\n\n\tdir := filepath.Dir(path)\n\tif err := fs.MkdirAll(dir, 0o755); err != nil {\n\t\treturn err\n\t}\n\n\tif cfg.Organizations == nil {\n\t\tcfg.Organizations = make(map[string]orgConfig)\n\t}\n\n\tdata, err := yaml.Marshal(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn afero.WriteFile(fs, path, data, 0o600)\n}\n\nfunc (cfg fileConfig) getToken(org string) string {\n\tif org == \"\" {\n\t\treturn \"\"\n\t}\n\tif cfg.Organizations == nil {\n\t\treturn \"\"\n\t}\n\tif v, ok := cfg.Organizations[org]; ok {\n\t\treturn v.APIToken\n\t}\n\treturn \"\"\n}\n\nfunc (conf *Config) writeUser() error {\n\treturn writeFileConfig(conf.fs, conf.userPath, conf.user)\n}\n\nfunc (conf *Config) writeLocal() error {\n\treturn writeFileConfig(conf.fs, conf.localPath, conf.local)\n}\n"
  },
  {
    "path": "internal/config/config_test.go",
    "content": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/pkg/keyring\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc setEnv(t *testing.T, key, value string) {\n\toriginal, had := os.LookupEnv(key)\n\tif err := os.Setenv(key, value); err != nil {\n\t\tt.Fatalf(\"failed to set env %s: %v\", key, err)\n\t}\n\tt.Cleanup(func() {\n\t\tvar restoreErr error\n\t\tif had {\n\t\t\trestoreErr = os.Setenv(key, original)\n\t\t} else {\n\t\t\trestoreErr = os.Unsetenv(key)\n\t\t}\n\t\tif restoreErr != nil {\n\t\t\tt.Fatalf(\"failed to restore env %s: %v\", key, restoreErr)\n\t\t}\n\t})\n}\n\nfunc unsetEnv(t *testing.T, key string) {\n\toriginal, had := os.LookupEnv(key)\n\tos.Unsetenv(key)\n\tt.Cleanup(func() {\n\t\tif had {\n\t\t\tos.Setenv(key, original)\n\t\t}\n\t})\n}\n\nfunc prepareTestDirectory(fs afero.Fs, fixturePath, configPath string) error {\n\t// read the content of the fixture config file from the real filesystem\n\tin, err := os.ReadFile(filepath.Join(\"../../fixtures/config\", fixturePath))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// create the config file in the afero filesystem\n\terr = fs.MkdirAll(filepath.Dir(configPath), os.ModePerm)\n\tif err != nil {\n\t\treturn err\n\t}\n\tout, err := fs.Create(configPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer out.Close()\n\t_, err = out.Write(in)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc TestConfig(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"read in local config\", func(t *testing.T) {\n\t\tfs := afero.NewMemMapFs()\n\t\tsetEnv(t, \"BUILDKITE_ORGANIZATION_SLUG\", \"\")\n\t\tsetEnv(t, \"BUILDKITE_API_TOKEN\", \"\")\n\t\terr := prepareTestDirectory(fs, \"local.basic.yaml\", localConfigFilePath)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// try to load configuration\n\t\tconf := New(fs, nil)\n\n\t\tif got := conf.OrganizationSlug(); got != \"buildkite-test\" {\n\t\t\tt.Errorf(\"OrganizationSlug() does not match: %s\", got)\n\t\t}\n\t\tif got := conf.APIToken(); got != \"test-token-1234\" {\n\t\t\tt.Errorf(\"APIToken() does not match: %s\", got)\n\t\t}\n\t\tif got := conf.PreferredPipelines(); len(got) != 2 {\n\t\t\tt.Errorf(\"PreferredPipelines() does not match: %d\", len(got))\n\t\t}\n\t})\n\n\tt.Run(\"APITokenForOrg reads legacy tokens from config\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tsetEnv(t, \"BUILDKITE_API_TOKEN\", \"\")\n\n\t\tfs := afero.NewMemMapFs()\n\t\t// Write a config with legacy token entries\n\t\tcontent := []byte(\"organizations:\\n  org1:\\n    api_token: token-org1\\n  org2:\\n    api_token: token-org2\\n\")\n\t\tif err := afero.WriteFile(fs, configFile(), content, 0o600); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tconf := New(fs, nil)\n\n\t\tif conf.APITokenForOrg(\"org1\") != \"token-org1\" {\n\t\t\tt.Errorf(\"expected token-org1, got %s\", conf.APITokenForOrg(\"org1\"))\n\t\t}\n\t\tif conf.APITokenForOrg(\"org2\") != \"token-org2\" {\n\t\t\tt.Errorf(\"expected token-org2, got %s\", conf.APITokenForOrg(\"org2\"))\n\t\t}\n\t\tif conf.APITokenForOrg(\"nonexistent\") != \"\" {\n\t\t\tt.Errorf(\"expected empty token for nonexistent org, got %s\", conf.APITokenForOrg(\"nonexistent\"))\n\t\t}\n\t})\n\n\tt.Run(\"loadFileConfig returns error on invalid yaml\", func(t *testing.T) {\n\t\tfs := afero.NewMemMapFs()\n\t\tpath := filepath.Join(t.TempDir(), \"bk.yaml\")\n\t\tif err := afero.WriteFile(fs, path, []byte(\"selected_org: [oops\"), 0o600); err != nil {\n\t\t\tt.Fatalf(\"failed to write invalid yaml: %v\", err)\n\t\t}\n\n\t\t_, err := loadFileConfig(fs, path)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"expected error for invalid yaml, got nil\")\n\t\t}\n\t})\n\n\tt.Run(\"loadFileConfig ignores missing file\", func(t *testing.T) {\n\t\tfs := afero.NewMemMapFs()\n\t\t_, err := loadFileConfig(fs, \"does-not-exist.yaml\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error for missing file, got %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"preserves organization name case\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestCases := []struct {\n\t\t\tname    string\n\t\t\torgName string\n\t\t}{\n\t\t\t{\n\t\t\t\tname:    \"mixed case organization name\",\n\t\t\t\torgName: \"gridX\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:    \"uppercase organization name\",\n\t\t\t\torgName: \"ACME\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:    \"lowercase organization name\",\n\t\t\t\torgName: \"buildkite\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:    \"camelCase organization name\",\n\t\t\t\torgName: \"myOrg\",\n\t\t\t},\n\t\t}\n\n\t\tfor _, tc := range testCases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\tfs := afero.NewMemMapFs()\n\t\t\t\tconf := New(fs, nil)\n\n\t\t\t\t// Register organization\n\t\t\t\tif err := conf.EnsureOrganization(tc.orgName); err != nil {\n\t\t\t\t\tt.Fatalf(\"EnsureOrganization failed: %v\", err)\n\t\t\t\t}\n\n\t\t\t\t// Select organization (simulate user config scenario)\n\t\t\t\tif err := conf.SelectOrganization(tc.orgName, false); err != nil {\n\t\t\t\t\tt.Fatalf(\"SelectOrganization failed: %v\", err)\n\t\t\t\t}\n\n\t\t\t\t// Create a new config instance to simulate reading from file\n\t\t\t\tconf2 := New(fs, nil)\n\n\t\t\t\t// Verify organization name case is preserved\n\t\t\t\tgotOrg := conf2.OrganizationSlug()\n\t\t\t\tif gotOrg != tc.orgName {\n\t\t\t\t\tt.Errorf(\"expected organization slug %q, got %q - case was not preserved\", tc.orgName, gotOrg)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"OutputFormat returns correct precedence\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tt.Run(\"defaults to json\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tsetEnv(t, \"BUILDKITE_OUTPUT_FORMAT\", \"\")\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := New(fs, nil)\n\n\t\t\tif got := conf.OutputFormat(); got != \"json\" {\n\t\t\t\tt.Errorf(\"OutputFormat() = %q, want %q\", got, \"json\")\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"env overrides config\", func(t *testing.T) {\n\t\t\tsetEnv(t, \"BUILDKITE_OUTPUT_FORMAT\", \"yaml\")\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := New(fs, nil)\n\t\t\tconf.SetOutputFormat(\"text\", false)\n\n\t\t\tif got := conf.OutputFormat(); got != \"yaml\" {\n\t\t\t\tt.Errorf(\"OutputFormat() = %q, want %q (env should override)\", got, \"yaml\")\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"config value is used\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tsetEnv(t, \"BUILDKITE_OUTPUT_FORMAT\", \"\")\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := New(fs, nil)\n\t\t\tconf.SetOutputFormat(\"yaml\", false)\n\n\t\t\tif got := conf.OutputFormat(); got != \"yaml\" {\n\t\t\t\tt.Errorf(\"OutputFormat() = %q, want %q\", got, \"yaml\")\n\t\t\t}\n\t\t})\n\t})\n\n\tt.Run(\"Quiet returns correct precedence\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tt.Run(\"defaults to false\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tsetEnv(t, \"BUILDKITE_QUIET\", \"\")\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := New(fs, nil)\n\n\t\t\tif conf.Quiet() {\n\t\t\t\tt.Error(\"Quiet() = true, want false\")\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"env overrides config\", func(t *testing.T) {\n\t\t\tsetEnv(t, \"BUILDKITE_QUIET\", \"true\")\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := New(fs, nil)\n\n\t\t\tif !conf.Quiet() {\n\t\t\t\tt.Error(\"Quiet() = false, want true (env should override)\")\n\t\t\t}\n\t\t})\n\t})\n\n\tt.Run(\"NoInput returns correct precedence\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tt.Run(\"defaults to false\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tsetEnv(t, \"BUILDKITE_NO_INPUT\", \"\")\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := New(fs, nil)\n\n\t\t\tif conf.NoInput() {\n\t\t\t\tt.Error(\"NoInput() = true, want false\")\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"env overrides config\", func(t *testing.T) {\n\t\t\tsetEnv(t, \"BUILDKITE_NO_INPUT\", \"true\")\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := New(fs, nil)\n\n\t\t\tif !conf.NoInput() {\n\t\t\t\tt.Error(\"NoInput() = false, want true (env should override)\")\n\t\t\t}\n\t\t})\n\t})\n\n\tt.Run(\"Pager returns correct precedence\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tt.Run(\"defaults to less -R\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tsetEnv(t, \"PAGER\", \"\")\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := New(fs, nil)\n\n\t\t\tif got := conf.Pager(); got != \"less -R\" {\n\t\t\t\tt.Errorf(\"Pager() = %q, want %q\", got, \"less -R\")\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"env overrides config\", func(t *testing.T) {\n\t\t\tsetEnv(t, \"PAGER\", \"more\")\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := New(fs, nil)\n\t\t\tconf.SetPager(\"vim\")\n\n\t\t\tif got := conf.Pager(); got != \"more\" {\n\t\t\t\tt.Errorf(\"Pager() = %q, want %q (env should override)\", got, \"more\")\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"config value is used\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tsetEnv(t, \"PAGER\", \"\")\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := New(fs, nil)\n\t\t\tconf.SetPager(\"vim\")\n\n\t\t\tif got := conf.Pager(); got != \"vim\" {\n\t\t\t\tt.Errorf(\"Pager() = %q, want %q\", got, \"vim\")\n\t\t\t}\n\t\t})\n\t})\n}\n\nfunc TestAPITokenForOrgNoKeyring(t *testing.T) {\n\t// Ensure BUILDKITE_NO_KEYRING disables keychain access entirely and that\n\t// APITokenForOrg falls through to the config file (legacy) path without\n\t// attempting to call the OS keychain.\n\tsetEnv(t, \"BUILDKITE_NO_KEYRING\", \"1\")\n\tsetEnv(t, \"CI\", \"\")\n\tsetEnv(t, \"BUILDKITE\", \"\")\n\tsetEnv(t, \"BUILDKITE_API_TOKEN\", \"\")\n\tkeyring.ResetForTesting()\n\tt.Cleanup(keyring.ResetForTesting)\n\n\tfs := afero.NewMemMapFs()\n\tcontent := []byte(\"organizations:\\n  my-org:\\n    api_token: legacy-token\\n\")\n\tif err := afero.WriteFile(fs, configFile(), content, 0o600); err != nil {\n\t\tt.Fatalf(\"failed to write config: %v\", err)\n\t}\n\n\tconf := New(fs, nil)\n\n\t// Should return the legacy file token without touching the keychain.\n\tif got := conf.APITokenForOrg(\"my-org\"); got != \"legacy-token\" {\n\t\tt.Errorf(\"APITokenForOrg() = %q, want %q\", got, \"legacy-token\")\n\t}\n\n\t// Keyring must report unavailable.\n\tkr := keyring.New()\n\tif kr.IsAvailable() {\n\t\tt.Error(\"expected keyring to be unavailable when BUILDKITE_NO_KEYRING=1\")\n\t}\n}\n\nfunc TestExperiments(t *testing.T) {\n\tt.Run(\"defaults to preflight\", func(t *testing.T) {\n\t\tunsetEnv(t, \"BUILDKITE_EXPERIMENTS\")\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := New(fs, nil)\n\n\t\tif got := conf.Experiments(); got != DefaultExperiments {\n\t\t\tt.Errorf(\"Experiments() = %q, want %q\", got, DefaultExperiments)\n\t\t}\n\t})\n\n\tt.Run(\"env overrides config\", func(t *testing.T) {\n\t\tsetEnv(t, \"BUILDKITE_EXPERIMENTS\", \"alpha\")\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := New(fs, nil)\n\t\tconf.SetExperiments(\"beta\")\n\n\t\tif got := conf.Experiments(); got != \"alpha\" {\n\t\t\tt.Errorf(\"Experiments() = %q, want %q (env should override)\", got, \"alpha\")\n\t\t}\n\t})\n\n\tt.Run(\"env empty string does not fall through\", func(t *testing.T) {\n\t\tsetEnv(t, \"BUILDKITE_EXPERIMENTS\", \"\")\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := New(fs, nil)\n\t\tconf.SetExperiments(\"beta\")\n\n\t\tif got := conf.Experiments(); got != \"\" {\n\t\t\tt.Errorf(\"Experiments() = %q, want %q (empty env should not fall through)\", got, \"\")\n\t\t}\n\t})\n\n\tt.Run(\"config overrides the default\", func(t *testing.T) {\n\t\tunsetEnv(t, \"BUILDKITE_EXPERIMENTS\")\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := New(fs, nil)\n\t\tconf.SetExperiments(\"beta\")\n\n\t\tif got := conf.Experiments(); got != \"beta\" {\n\t\t\tt.Errorf(\"Experiments() = %q, want %q\", got, \"beta\")\n\t\t}\n\t})\n\n\tt.Run(\"SetExperiments persists\", func(t *testing.T) {\n\t\tunsetEnv(t, \"BUILDKITE_EXPERIMENTS\")\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := New(fs, nil)\n\n\t\tif err := conf.SetExperiments(\"preflight,beta\"); err != nil {\n\t\t\tt.Fatalf(\"SetExperiments() error: %v\", err)\n\t\t}\n\n\t\tconf2 := New(fs, nil)\n\t\tif got := conf2.Experiments(); got != \"preflight,beta\" {\n\t\t\tt.Errorf(\"Experiments() after reload = %q, want %q\", got, \"preflight,beta\")\n\t\t}\n\t})\n}\n\nfunc TestHasExperimentEnvOverride(t *testing.T) {\n\tt.Run(\"empty env override disables default experiments\", func(t *testing.T) {\n\t\tsetEnv(t, \"BUILDKITE_EXPERIMENTS\", \"\")\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := New(fs, nil)\n\n\t\tif conf.HasExperiment(ExperimentPreflight) {\n\t\t\tt.Errorf(\"HasExperiment(%q) = true, want false\", ExperimentPreflight)\n\t\t}\n\t})\n}\n\nfunc TestHasExperiment(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\texperiments string\n\t\tquery       string\n\t\twant        bool\n\t}{\n\t\t{\"preflight defaults on\", \"\", ExperimentPreflight, true},\n\t\t{\"single match\", \"preflight\", \"preflight\", true},\n\t\t{\"multiple with match\", \"foo,preflight,bar\", \"preflight\", true},\n\t\t{\"override without match\", \"foo,bar\", \"preflight\", false},\n\t\t{\"whitespace handling\", \" preflight , bar \", \"preflight\", true},\n\t\t{\"other experiments still default off\", \"\", \"beta\", false},\n\t\t{\"partial name no match\", \"preflightx\", \"preflight\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tunsetEnv(t, \"BUILDKITE_EXPERIMENTS\")\n\n\t\t\tfs := afero.NewMemMapFs()\n\t\t\tconf := New(fs, nil)\n\t\t\tconf.SetExperiments(tt.experiments)\n\n\t\t\tif got := conf.HasExperiment(tt.query); got != tt.want {\n\t\t\t\tt.Errorf(\"HasExperiment(%q) with experiments=%q: got %v, want %v\", tt.query, tt.experiments, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/emoji/emoji.go",
    "content": "package emoji\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/buildkite/termoji\"\n)\n\nvar (\n\tonce     sync.Once\n\trenderer *termoji.Renderer\n\n\t// leadingShortcodes matches one or more :shortcode: tokens (with\n\t// optional whitespace between them) anchored at the start of the string.\n\tleadingShortcodes = regexp.MustCompile(`^(:[[:alnum:]_.+-]+:\\s*)+`)\n)\n\nfunc getRenderer() *termoji.Renderer {\n\tonce.Do(func() {\n\t\tr, err := termoji.New(termoji.Options{})\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\trenderer = r\n\t})\n\treturn renderer\n}\n\n// Render expands emoji shortcodes in text using termoji. Standard\n// Unicode emoji (e.g. :checkered_flag:) are converted to their Unicode\n// code points on all terminals. Buildkite custom emoji (e.g. :docker:)\n// are rendered as inline images on terminals that support the iTerm2 or\n// Kitty graphics protocol; on other terminals they are left unchanged.\n//\n// Because inline-image escape sequences embed foreground-colour resets,\n// callers should not wrap the result in ANSI foreground styling (e.g.\n// lipgloss). For coloured output, use [Split] to separate the emoji\n// prefix so it can be rendered outside the colour span.\nfunc Render(text string) string {\n\tif r := getRenderer(); r != nil {\n\t\treturn r.Render(text)\n\t}\n\treturn text\n}\n\n// Split separates leading emoji shortcodes from the rest of the text.\n// Whitespace between the emoji and text is trimmed from both sides so\n// callers can control spacing consistently.\n//\n//\tSplit(\":docker: Build image\")         → (\":docker:\", \"Build image\")\n//\tSplit(\":docker: :go: Build\")          → (\":docker: :go:\", \"Build\")\n//\tSplit(\"Build image\")                  → (\"\", \"Build image\")\n//\tSplit(\":pipeline:\")                   → (\":pipeline:\", \"\")\n//\n// This is useful for coloured output: render the prefix with [Render]\n// outside the ANSI colour span, and style only the rest.\nfunc Split(text string) (prefix, rest string) {\n\tloc := leadingShortcodes.FindStringIndex(text)\n\tif loc == nil {\n\t\treturn \"\", text\n\t}\n\treturn strings.TrimRight(text[:loc[1]], \" \\t\"), text[loc[1]:]\n}\n"
  },
  {
    "path": "internal/emoji/emoji_test.go",
    "content": "package emoji\n\nimport (\n\t\"testing\"\n)\n\nfunc TestRender_standardEmoji(t *testing.T) {\n\tgot := Render(\":checkered_flag: Feature flags\")\n\twant := \"🏁 Feature flags\"\n\tif got != want {\n\t\tt.Errorf(\"Render(%q) = %q, want %q\", \":checkered_flag: Feature flags\", got, want)\n\t}\n}\n\nfunc TestRender_plainText(t *testing.T) {\n\tgot := Render(\"just plain text\")\n\tif got != \"just plain text\" {\n\t\tt.Errorf(\"Render(%q) = %q, want unchanged\", \"just plain text\", got)\n\t}\n}\n\nfunc TestSplit(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tinput      string\n\t\twantPrefix string\n\t\twantRest   string\n\t}{\n\t\t{\n\t\t\tname:       \"single shortcode with text\",\n\t\t\tinput:      \":docker: Build image\",\n\t\t\twantPrefix: \":docker:\",\n\t\t\twantRest:   \"Build image\",\n\t\t},\n\t\t{\n\t\t\tname:       \"multiple shortcodes\",\n\t\t\tinput:      \":docker: :golang: Build\",\n\t\t\twantPrefix: \":docker: :golang:\",\n\t\t\twantRest:   \"Build\",\n\t\t},\n\t\t{\n\t\t\tname:       \"no shortcodes\",\n\t\t\tinput:      \"Build image\",\n\t\t\twantPrefix: \"\",\n\t\t\twantRest:   \"Build image\",\n\t\t},\n\t\t{\n\t\t\tname:       \"shortcode only\",\n\t\t\tinput:      \":pipeline:\",\n\t\t\twantPrefix: \":pipeline:\",\n\t\t\twantRest:   \"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"shortcode with hyphen\",\n\t\t\tinput:      \":golangci-lint: lint\",\n\t\t\twantPrefix: \":golangci-lint:\",\n\t\t\twantRest:   \"lint\",\n\t\t},\n\t\t{\n\t\t\tname:       \"shortcode in middle not matched\",\n\t\t\tinput:      \"Build :docker: image\",\n\t\t\twantPrefix: \"\",\n\t\t\twantRest:   \"Build :docker: image\",\n\t\t},\n\t\t{\n\t\t\tname:       \"empty string\",\n\t\t\tinput:      \"\",\n\t\t\twantPrefix: \"\",\n\t\t\twantRest:   \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tprefix, rest := Split(tt.input)\n\t\t\tif prefix != tt.wantPrefix || rest != tt.wantRest {\n\t\t\t\tt.Errorf(\"Split(%q) = (%q, %q), want (%q, %q)\",\n\t\t\t\t\ttt.input, prefix, rest, tt.wantPrefix, tt.wantRest)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/errors/README.md",
    "content": "# Error Handling Package\n\nThis package provides standardized error handling for the Buildkite CLI. It handles different types of errors consistently, provides helpful error messages and suggestions to users, and ensures a unified approach to error reporting across the application.\n\n## Features\n\n- **Categorized Errors**: Different error types (validation, API, not found, etc.) with specific handling\n- **Contextual Error Messages**: Errors include context about what operation failed\n- **Helpful Suggestions**: Error messages include suggestions on how to fix the issue\n- **Command Integration**: Easy integration with Kong commands\n- **API Error Handling**: Specialized handling for API errors with status code interpretation\n- **Exit Codes**: Appropriate exit codes for different error types\n\n## Usage\n\n### Creating Errors\n\n```go\nimport (\n    bkErrors \"github.com/buildkite/cli/v3/internal/errors\"\n)\n\n// Create various types of errors\nvalidationErr := bkErrors.NewValidationError(\n    err, // Original error (can be nil)\n    \"Invalid input\", // Details about the error\n    \"Try using a different value\", // Suggestions (optional)\n    \"Check the documentation for valid options\" // More suggestions\n)\n\napiErr := bkErrors.NewAPIError(err, \"API request failed\")\n\nnotFoundErr := bkErrors.NewResourceNotFoundError(err, \"Resource not found\")\n```\n\n### Adding Context to Errors\n\n```go\n// Add suggestions to an existing error\nerr = bkErrors.WithSuggestions(err,\n    \"Try this instead\",\n    \"Or try this other option\"\n)\n\n// Add details to an existing error\nerr = bkErrors.WithDetails(err, \"Additional context\")\n```\n\n### Checking Error Types\n\n```go\nif bkErrors.IsNotFound(err) {\n    // Handle not found error\n}\n\nif bkErrors.IsValidationError(err) {\n    // Handle validation error\n}\n\nif bkErrors.IsAPIError(err) {\n    // Handle API error\n}\n```\n\n### Wrapping API Errors\n\n```go\n// Wrap HTTP errors with appropriate context\nerr = bkErrors.WrapAPIError(err, \"fetching pipeline\")\n```\n\n### Command Integration\n\n```go\n// Wrap a command's RunE function with standard error handling\ncmd := &cobra.Command{\n    RunE: bkErrors.WrapRunE(func(cmd *cobra.Command, args []string) error {\n        // Command implementation\n        return err\n    }),\n}\n```\n\n### Using the Error Handler\n\n```go\n// Create an error handler\nhandler := bkErrors.NewHandler().\n    WithVerbose(verbose).\n    WithWriter(os.Stderr)\n\n// Handle an error\nhandler.Handle(err)\n\n// Handle an error with operation context\nhandler.HandleWithDetails(err, \"creating resource\")\n\n// Print a warning\nhandler.PrintWarning(\"Something might be wrong: %s\", details)\n```\n\n### Using the Command Error Handler\n\n```go\n// In main.go:\nfunc main() {\n    rootCmd, _ := root.NewCmdRoot(f)\n\n    // Execute with error handling\n    bkErrors.ExecuteWithErrorHandling(rootCmd, verbose)\n}\n```\n"
  },
  {
    "path": "internal/errors/api.go",
    "content": "package errors\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\thttpClient \"github.com/buildkite/cli/v3/internal/http\"\n)\n\n// APIErrorResponse represents a Buildkite API error response\ntype APIErrorResponse struct {\n\tMessage string            `json:\"message\"`\n\tErrors  []string          `json:\"errors,omitempty\"`\n\tDetails map[string]string `json:\"details,omitempty\"`\n}\n\n// WrapAPIError wraps an API error with appropriate context and suggestions\nfunc WrapAPIError(err error, operation string) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\t// If it's already a CLI error, add context but preserve the category\n\tif cliErr, ok := err.(*Error); ok {\n\t\tif operation != \"\" {\n\t\t\tcliErr.Details = fmt.Sprintf(\"%s: %s\", operation, cliErr.Details)\n\t\t}\n\t\treturn cliErr\n\t}\n\n\t// Handle HTTP client errors\n\tif httpErr, ok := err.(*httpClient.ErrorResponse); ok {\n\t\treturn handleHTTPError(httpErr, operation)\n\t}\n\n\t// For all other errors, wrap as a generic API error\n\treturn NewAPIError(err, fmt.Sprintf(\"API request failed during: %s\", operation))\n}\n\n// handleHTTPError processes an HTTP error and creates an appropriate CLI error\nfunc handleHTTPError(httpErr *httpClient.ErrorResponse, operation string) error {\n\tstatusCode := httpErr.StatusCode\n\tdetails := fmt.Sprintf(\"%s failed with status %d\", operation, statusCode)\n\n\t// Try to parse the response body as JSON\n\tvar apiErr APIErrorResponse\n\tif len(httpErr.Body) > 0 {\n\t\tif err := json.Unmarshal(httpErr.Body, &apiErr); err == nil {\n\t\t\t// Successfully parsed API error\n\t\t\tif apiErr.Message != \"\" {\n\t\t\t\tdetails = fmt.Sprintf(\"%s: %s\", details, apiErr.Message)\n\t\t\t}\n\t\t} else {\n\t\t\t// If not JSON, include the raw body\n\t\t\tif len(httpErr.Body) > 0 {\n\t\t\t\tbodyStr := string(httpErr.Body)\n\t\t\t\tif len(bodyStr) > 200 {\n\t\t\t\t\tbodyStr = bodyStr[:200] + \"...\"\n\t\t\t\t}\n\t\t\t\tdetails = fmt.Sprintf(\"%s: %s\", details, bodyStr)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Create appropriate error based on status code\n\tvar err error\n\tswitch {\n\tcase statusCode == http.StatusNotFound:\n\t\terr = NewResourceNotFoundError(httpErr, details, suggestForNotFound(httpErr.URL)...)\n\tcase statusCode == http.StatusUnauthorized:\n\t\terr = NewAuthenticationError(httpErr, details,\n\t\t\t\"Check your API token in the configuration\",\n\t\t\t\"Run 'bk configure' to set up your token correctly\")\n\tcase statusCode == http.StatusForbidden:\n\t\terr = NewPermissionDeniedError(httpErr, details,\n\t\t\t\"Verify that your API token has the required scopes\",\n\t\t\t\"You may need to create a new token with additional permissions\")\n\tcase statusCode == http.StatusBadRequest:\n\t\terr = handleBadRequestError(httpErr, details, apiErr)\n\tcase statusCode >= 500:\n\t\terr = NewAPIError(httpErr, details,\n\t\t\t\"This appears to be a server-side error\",\n\t\t\t\"Try again later or check the Buildkite status page\")\n\tdefault:\n\t\terr = NewAPIError(httpErr, details)\n\t}\n\n\treturn err\n}\n\n// handleBadRequestError processes a 400 Bad Request error\nfunc handleBadRequestError(httpErr *httpClient.ErrorResponse, details string, apiErr APIErrorResponse) error {\n\tsuggestions := []string{}\n\n\t// Add specific errors as suggestions\n\tif len(apiErr.Errors) > 0 {\n\t\tsuggestions = append(suggestions, apiErr.Errors...)\n\t} else {\n\t\tsuggestions = append(suggestions, \"Check the request parameters for invalid values\")\n\t}\n\n\t// Add field-specific errors\n\tif len(apiErr.Details) > 0 {\n\t\tfor field, msg := range apiErr.Details {\n\t\t\tsuggestions = append(suggestions, fmt.Sprintf(\"%s: %s\", field, msg))\n\t\t}\n\t}\n\n\treturn NewValidationError(httpErr, details, suggestions...)\n}\n\n// suggestForNotFound generates suggestions for a 404 Not Found error\nfunc suggestForNotFound(url string) []string {\n\tsuggestions := []string{\n\t\t\"Check that the resource exists and you have access to it\",\n\t}\n\n\t// Add more specific suggestions based on the URL pattern\n\tif strings.Contains(url, \"/pipelines/\") {\n\t\tsuggestions = append(suggestions, \"Verify the pipeline slug is correct\")\n\t} else if strings.Contains(url, \"/builds/\") {\n\t\tsuggestions = append(suggestions, \"Verify the build number is correct\")\n\t} else if strings.Contains(url, \"/artifacts/\") {\n\t\tsuggestions = append(suggestions, \"Verify the artifact ID is correct\")\n\t} else if strings.Contains(url, \"/agents/\") {\n\t\tsuggestions = append(suggestions, \"Verify the agent ID is correct\")\n\t}\n\n\treturn suggestions\n}\n"
  },
  {
    "path": "internal/errors/api_test.go",
    "content": "package errors\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\thttpClient \"github.com/buildkite/cli/v3/internal/http\"\n)\n\nfunc TestWrapAPIError(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"handles nil error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tresult := WrapAPIError(nil, \"test operation\")\n\t\tif result != nil {\n\t\t\tt.Errorf(\"Expected nil, got %v\", result)\n\t\t}\n\t})\n\n\tt.Run(\"preserves CLI error category\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\toriginal := NewValidationError(nil, \"Invalid input\")\n\t\tresult := WrapAPIError(original, \"test operation\")\n\n\t\tif !IsValidationError(result) {\n\t\t\tt.Error(\"Expected validation error category to be preserved\")\n\t\t}\n\t})\n\n\tt.Run(\"adds operation context to CLI error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\toriginal := NewValidationError(nil, \"Invalid input\")\n\t\tresult := WrapAPIError(original, \"test operation\")\n\n\t\tcliErr, ok := result.(*Error)\n\t\tif !ok {\n\t\t\tt.Fatal(\"Expected result to be a *Error\")\n\t\t}\n\n\t\tif cliErr.Details != \"test operation: Invalid input\" {\n\t\t\tt.Errorf(\"Expected details to include operation, got: %q\", cliErr.Details)\n\t\t}\n\t})\n\n\tt.Run(\"wraps generic error as API error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\toriginal := &simpleError{message: \"something went wrong\"}\n\t\tresult := WrapAPIError(original, \"test operation\")\n\n\t\tif !IsAPIError(result) {\n\t\t\tt.Error(\"Expected generic error to be wrapped as API error\")\n\t\t}\n\t})\n}\n\nfunc TestHandleHTTPError(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"handles 404 not found\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thttpErr := &httpClient.ErrorResponse{\n\t\t\tStatusCode: 404,\n\t\t\tStatus:     \"Not Found\",\n\t\t\tURL:        \"https://api.buildkite.com/v2/pipelines/missing\",\n\t\t\tBody:       []byte(`{\"message\":\"Pipeline not found\"}`),\n\t\t}\n\n\t\tresult := handleHTTPError(httpErr, \"get pipeline\")\n\n\t\tif !IsNotFound(result) {\n\t\t\tt.Error(\"Expected result to be a not found error\")\n\t\t}\n\n\t\t// Check for pipeline-specific suggestion\n\t\tcliErr, ok := result.(*Error)\n\t\tif !ok {\n\t\t\tt.Fatal(\"Expected result to be a *Error\")\n\t\t}\n\n\t\tfoundSuggestion := false\n\t\tfor _, suggestion := range cliErr.Suggestions {\n\t\t\tif suggestion == \"Verify the pipeline slug is correct\" {\n\t\t\t\tfoundSuggestion = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !foundSuggestion {\n\t\t\tt.Error(\"Expected pipeline-specific suggestion for not found error\")\n\t\t}\n\t})\n\n\tt.Run(\"handles 401 unauthorized\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thttpErr := &httpClient.ErrorResponse{\n\t\t\tStatusCode: 401,\n\t\t\tStatus:     \"Unauthorized\",\n\t\t\tURL:        \"https://api.buildkite.com/v2/user\",\n\t\t\tBody:       []byte(`{\"message\":\"Unauthorized\"}`),\n\t\t}\n\n\t\tresult := handleHTTPError(httpErr, \"get user\")\n\n\t\tif !IsAuthenticationError(result) {\n\t\t\tt.Error(\"Expected result to be an authentication error\")\n\t\t}\n\n\t\t// Check for token suggestion\n\t\tcliErr, ok := result.(*Error)\n\t\tif !ok {\n\t\t\tt.Fatal(\"Expected result to be a *Error\")\n\t\t}\n\n\t\tfoundSuggestion := false\n\t\tfor _, suggestion := range cliErr.Suggestions {\n\t\t\tif suggestion == \"Check your API token in the configuration\" {\n\t\t\t\tfoundSuggestion = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !foundSuggestion {\n\t\t\tt.Error(\"Expected token-specific suggestion for unauthorized error\")\n\t\t}\n\t})\n\n\tt.Run(\"handles 403 forbidden\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thttpErr := &httpClient.ErrorResponse{\n\t\t\tStatusCode: 403,\n\t\t\tStatus:     \"Forbidden\",\n\t\t\tURL:        \"https://api.buildkite.com/v2/builds\",\n\t\t\tBody:       []byte(`{\"message\":\"Forbidden\"}`),\n\t\t}\n\n\t\tresult := handleHTTPError(httpErr, \"cancel build\")\n\n\t\tif !IsPermissionDeniedError(result) {\n\t\t\tt.Error(\"Expected result to be a permission denied error\")\n\t\t}\n\t})\n\n\tt.Run(\"handles 400 bad request with field errors\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tapiErr := APIErrorResponse{\n\t\t\tMessage: \"Invalid request\",\n\t\t\tDetails: map[string]string{\n\t\t\t\t\"name\": \"cannot be blank\",\n\t\t\t\t\"url\":  \"invalid format\",\n\t\t\t},\n\t\t}\n\n\t\tbody, _ := json.Marshal(apiErr)\n\t\thttpErr := &httpClient.ErrorResponse{\n\t\t\tStatusCode: 400,\n\t\t\tStatus:     \"Bad Request\",\n\t\t\tURL:        \"https://api.buildkite.com/v2/pipelines\",\n\t\t\tBody:       body,\n\t\t}\n\n\t\tresult := handleHTTPError(httpErr, \"create pipeline\")\n\n\t\tif !IsValidationError(result) {\n\t\t\tt.Error(\"Expected result to be a validation error\")\n\t\t}\n\n\t\t// Check that field errors are included in suggestions\n\t\tcliErr, ok := result.(*Error)\n\t\tif !ok {\n\t\t\tt.Fatal(\"Expected result to be a *Error\")\n\t\t}\n\n\t\tfoundNameError := false\n\t\tfoundURLError := false\n\t\tfor _, suggestion := range cliErr.Suggestions {\n\t\t\tif suggestion == \"name: cannot be blank\" {\n\t\t\t\tfoundNameError = true\n\t\t\t}\n\t\t\tif suggestion == \"url: invalid format\" {\n\t\t\t\tfoundURLError = true\n\t\t\t}\n\t\t}\n\n\t\tif !foundNameError || !foundURLError {\n\t\t\tt.Error(\"Expected field-specific errors to be included in suggestions\")\n\t\t}\n\t})\n\n\tt.Run(\"handles 500 server error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thttpErr := &httpClient.ErrorResponse{\n\t\t\tStatusCode: 500,\n\t\t\tStatus:     \"Internal Server Error\",\n\t\t\tURL:        \"https://api.buildkite.com/v2/builds\",\n\t\t\tBody:       []byte(`{\"message\":\"Internal server error\"}`),\n\t\t}\n\n\t\tresult := handleHTTPError(httpErr, \"list builds\")\n\n\t\tif !IsAPIError(result) {\n\t\t\tt.Error(\"Expected result to be an API error\")\n\t\t}\n\n\t\t// Check for server error suggestion\n\t\tcliErr, ok := result.(*Error)\n\t\tif !ok {\n\t\t\tt.Fatal(\"Expected result to be a *Error\")\n\t\t}\n\n\t\tfoundSuggestion := false\n\t\tfor _, suggestion := range cliErr.Suggestions {\n\t\t\tif suggestion == \"This appears to be a server-side error\" {\n\t\t\t\tfoundSuggestion = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !foundSuggestion {\n\t\t\tt.Error(\"Expected server error suggestion\")\n\t\t}\n\t})\n}\n\n// simpleError is a simple implementation of the error interface for testing\ntype simpleError struct {\n\tmessage string\n}\n\nfunc (e *simpleError) Error() string {\n\treturn e.message\n}\n"
  },
  {
    "path": "internal/errors/errors.go",
    "content": "package errors\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Standard error types that can be used to categorize errors\nvar (\n\t// ErrConfiguration indicates an error in the user's configuration\n\tErrConfiguration = errors.New(\"configuration error\")\n\n\t// ErrValidation indicates invalid input from the user\n\tErrValidation = errors.New(\"validation error\")\n\n\t// ErrAPI indicates an error from the Buildkite API\n\tErrAPI = errors.New(\"API error\")\n\n\t// ErrResourceNotFound indicates a requested resource was not found\n\tErrResourceNotFound = errors.New(\"resource not found\")\n\n\t// ErrPermissionDenied indicates the user lacks permission\n\tErrPermissionDenied = errors.New(\"permission denied\")\n\n\t// ErrAuthentication indicates an issue with authentication\n\tErrAuthentication = errors.New(\"authentication error\")\n\n\t// ErrInternal indicates an internal error in the CLI\n\tErrInternal = errors.New(\"internal error\")\n\n\t// ErrSnapshot indicates an error creating a preflight snapshot\n\tErrSnapshot = errors.New(\"snapshot error\")\n\n\t// ErrUserAborted indicates the user has canceled an operation\n\tErrUserAborted = errors.New(\"user aborted\")\n\n\t// ErrPreflightCompletedFailure indicates a preflight build reached a terminal failed outcome.\n\tErrPreflightCompletedFailure = errors.New(\"preflight completed with failure\")\n\n\t// ErrPreflightIncompleteFailure indicates a preflight build has observed failures but is not terminal yet.\n\tErrPreflightIncompleteFailure = errors.New(\"preflight incomplete (failing)\")\n\n\t// ErrPreflightIncomplete indicates a preflight build is still in progress.\n\tErrPreflightIncomplete = errors.New(\"preflight incomplete\")\n\n\t// ErrPreflightUnknown indicates a preflight build returned an unknown result.\n\tErrPreflightUnknown = errors.New(\"preflight result unknown\")\n)\n\n// Error represents a CLI error with context\ntype Error struct {\n\t// Original is the underlying error\n\tOriginal error\n\n\t// Category is the broad category of the error\n\tCategory error\n\n\t// Details contains additional detail about the error\n\tDetails string\n\n\t// Suggestions provides hints on how to fix the error\n\tSuggestions []string\n}\n\n// Error implements the error interface\nfunc (e *Error) Error() string {\n\tvar msg strings.Builder\n\n\tif e.Category != nil {\n\t\tmsg.WriteString(e.Category.Error())\n\t\tmsg.WriteString(\": \")\n\t}\n\n\t// First include the original error, if present\n\tif e.Original != nil {\n\t\tmsg.WriteString(e.Original.Error())\n\t}\n\n\t// Then include details if present, regardless of whether Original is present\n\tif e.Details != \"\" {\n\t\t// Only add a separator if we've already written something\n\t\tif e.Original != nil {\n\t\t\tmsg.WriteString(\" (\")\n\t\t\tmsg.WriteString(e.Details)\n\t\t\tmsg.WriteString(\")\")\n\t\t} else {\n\t\t\tmsg.WriteString(e.Details)\n\t\t}\n\t}\n\n\treturn msg.String()\n}\n\n// FormattedError returns a formatted multi-line error message suitable for display\nfunc (e *Error) FormattedError() string {\n\tvar msg strings.Builder\n\n\t// Build the main error message\n\tif e.Category != nil {\n\t\t// Write category with uppercase first letter\n\t\tcategory := e.Category.Error()\n\t\tif len(category) > 0 {\n\t\t\tmsg.WriteString(strings.ToUpper(category[:1]) + category[1:])\n\t\t\tmsg.WriteString(\": \")\n\t\t}\n\t}\n\n\tif e.Original != nil {\n\t\tmsg.WriteString(e.Original.Error())\n\t} else if e.Details != \"\" {\n\t\tmsg.WriteString(e.Details)\n\t}\n\n\t// Add detailed suggestions if available\n\tif len(e.Suggestions) > 0 {\n\t\tmsg.WriteString(\"\\n\\n\")\n\t\tfor i, suggestion := range e.Suggestions {\n\t\t\tif i > 0 {\n\t\t\t\tmsg.WriteString(\"\\n\")\n\t\t\t}\n\t\t\tmsg.WriteString(\"• \")\n\t\t\tmsg.WriteString(suggestion)\n\t\t}\n\t}\n\n\treturn msg.String()\n}\n\n// Unwrap implements the errors.Unwrap interface to allow using errors.Is and errors.As\nfunc (e *Error) Unwrap() error {\n\tif e.Original != nil {\n\t\treturn e.Original\n\t}\n\treturn e.Category\n}\n\n// Is implements the errors.Is interface to allow checking error types\nfunc (e *Error) Is(target error) bool {\n\treturn errors.Is(e.Category, target) || (e.Original != nil && errors.Is(e.Original, target))\n}\n\n// NewError creates a new Error with the given attributes\nfunc NewError(original error, category error, details string, suggestions ...string) *Error {\n\treturn &Error{\n\t\tOriginal:    original,\n\t\tCategory:    category,\n\t\tDetails:     details,\n\t\tSuggestions: suggestions,\n\t}\n}\n\n// WithSuggestions adds suggestions to an existing error\nfunc WithSuggestions(err error, suggestions ...string) error {\n\tif cliErr, ok := err.(*Error); ok {\n\t\tcliErr.Suggestions = append(cliErr.Suggestions, suggestions...)\n\t\treturn cliErr\n\t}\n\n\t// If it's not already a CLI error, create a new one\n\treturn NewError(err, nil, \"\", suggestions...)\n}\n\n// WithDetails adds details to an existing error\nfunc WithDetails(err error, details string) error {\n\tif cliErr, ok := err.(*Error); ok {\n\t\tif cliErr.Details == \"\" {\n\t\t\tcliErr.Details = details\n\t\t} else {\n\t\t\tcliErr.Details = fmt.Sprintf(\"%s: %s\", cliErr.Details, details)\n\t\t}\n\t\treturn cliErr\n\t}\n\n\t// If it's not already a CLI error, create a new one\n\treturn NewError(err, nil, details)\n}\n\n// NewConfigurationError creates a new configuration error\nfunc NewConfigurationError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrConfiguration, details, suggestions...)\n}\n\n// NewValidationError creates a new validation error\nfunc NewValidationError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrValidation, details, suggestions...)\n}\n\n// NewAPIError creates a new API error\nfunc NewAPIError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrAPI, details, suggestions...)\n}\n\n// NewResourceNotFoundError creates a new resource not found error\nfunc NewResourceNotFoundError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrResourceNotFound, details, suggestions...)\n}\n\n// NewPermissionDeniedError creates a new permission denied error\nfunc NewPermissionDeniedError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrPermissionDenied, details, suggestions...)\n}\n\n// NewAuthenticationError creates a new authentication error\nfunc NewAuthenticationError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrAuthentication, details, suggestions...)\n}\n\n// NewInternalError creates a new internal error\nfunc NewInternalError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrInternal, details, suggestions...)\n}\n\n// NewSnapshotError creates a new snapshot error\nfunc NewSnapshotError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrSnapshot, details, suggestions...)\n}\n\n// NewUserAbortedError creates a new user aborted error\nfunc NewUserAbortedError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrUserAborted, details, suggestions...)\n}\n\n// NewPreflightCompletedFailureError creates a new completed preflight failure error.\nfunc NewPreflightCompletedFailureError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrPreflightCompletedFailure, details, suggestions...)\n}\n\n// NewPreflightIncompleteFailureError creates a new active preflight failure error.\nfunc NewPreflightIncompleteFailureError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrPreflightIncompleteFailure, details, suggestions...)\n}\n\n// NewPreflightIncompleteError creates a new incomplete preflight error.\nfunc NewPreflightIncompleteError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrPreflightIncomplete, details, suggestions...)\n}\n\n// NewPreflightUnknownError creates a new unknown preflight result error.\nfunc NewPreflightUnknownError(err error, details string, suggestions ...string) error {\n\treturn NewError(err, ErrPreflightUnknown, details, suggestions...)\n}\n\n// IsNotFound returns true if the error indicates a resource was not found\nfunc IsNotFound(err error) bool {\n\treturn errors.Is(err, ErrResourceNotFound)\n}\n\n// IsValidationError returns true if the error indicates a validation failure\nfunc IsValidationError(err error) bool {\n\treturn errors.Is(err, ErrValidation)\n}\n\n// IsAPIError returns true if the error indicates an API failure\nfunc IsAPIError(err error) bool {\n\treturn errors.Is(err, ErrAPI)\n}\n\n// IsAuthenticationError returns true if the error indicates an authentication failure\nfunc IsAuthenticationError(err error) bool {\n\treturn errors.Is(err, ErrAuthentication)\n}\n\n// IsPermissionDeniedError returns true if the error indicates a permission issue\nfunc IsPermissionDeniedError(err error) bool {\n\treturn errors.Is(err, ErrPermissionDenied)\n}\n\n// IsConfigurationError returns true if the error indicates a configuration issue\nfunc IsConfigurationError(err error) bool {\n\treturn errors.Is(err, ErrConfiguration)\n}\n\n// IsPreflightCompletedFailure returns true if the error indicates a terminal preflight failure.\nfunc IsPreflightCompletedFailure(err error) bool {\n\treturn errors.Is(err, ErrPreflightCompletedFailure)\n}\n\n// IsPreflightIncompleteFailure returns true if the error indicates an incomplete preflight failure.\nfunc IsPreflightIncompleteFailure(err error) bool {\n\treturn errors.Is(err, ErrPreflightIncompleteFailure)\n}\n\n// IsPreflightIncomplete returns true if the error indicates an incomplete preflight build.\nfunc IsPreflightIncomplete(err error) bool {\n\treturn errors.Is(err, ErrPreflightIncomplete)\n}\n\n// IsPreflightUnknown returns true if the error indicates an unknown preflight result.\nfunc IsPreflightUnknown(err error) bool {\n\treturn errors.Is(err, ErrPreflightUnknown)\n}\n\n// IsUserAborted returns true if the error indicates the user aborted the operation\nfunc IsUserAborted(err error) bool {\n\treturn errors.Is(err, ErrUserAborted)\n}\n"
  },
  {
    "path": "internal/errors/errors_test.go",
    "content": "package errors\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestErrorInterface(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"implements error interface\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\toriginalErr := fmt.Errorf(\"original error\")\n\t\terr := NewError(originalErr, ErrAPI, \"additional details\")\n\n\t\t// Check that Error() returns a non-empty string\n\t\tif err.Error() == \"\" {\n\t\t\tt.Error(\"Error() should return a non-empty string\")\n\t\t}\n\n\t\t// Check that the error string contains both the category and original error\n\t\terrStr := err.Error()\n\t\tif !strings.Contains(errStr, \"API error\") {\n\t\t\tt.Errorf(\"Error string %q should contain category 'API error'\", errStr)\n\t\t}\n\t\tif !strings.Contains(errStr, \"original error\") {\n\t\t\tt.Errorf(\"Error string %q should contain original error message\", errStr)\n\t\t}\n\t})\n\n\tt.Run(\"formatted error includes suggestions\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\toriginalErr := fmt.Errorf(\"resource not found\")\n\t\tsuggestions := []string{\"Check the resource name\", \"Verify you have access\"}\n\t\terr := NewError(originalErr, ErrResourceNotFound, \"failed to get resource\", suggestions...)\n\n\t\tformatted := err.FormattedError()\n\t\tfor _, suggestion := range suggestions {\n\t\t\tif !strings.Contains(formatted, suggestion) {\n\t\t\t\tt.Errorf(\"Formatted error should contain suggestion %q, got: %q\", suggestion, formatted)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestErrorCategorization(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"errors.Is works with standard error types\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapiErr := NewAPIError(nil, \"API request failed\")\n\t\tif !errors.Is(apiErr, ErrAPI) {\n\t\t\tt.Error(\"errors.Is should identify API error category\")\n\t\t}\n\n\t\tvalidationErr := NewValidationError(nil, \"Invalid input\")\n\t\tif !errors.Is(validationErr, ErrValidation) {\n\t\t\tt.Error(\"errors.Is should identify validation error category\")\n\t\t}\n\t})\n\n\tt.Run(\"error type checking functions work\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tapiErr := NewAPIError(nil, \"API request failed\")\n\t\tif !IsAPIError(apiErr) {\n\t\t\tt.Error(\"IsAPIError should return true for API errors\")\n\t\t}\n\n\t\tvalidationErr := NewValidationError(nil, \"Invalid input\")\n\t\tif !IsValidationError(validationErr) {\n\t\t\tt.Error(\"IsValidationError should return true for validation errors\")\n\t\t}\n\n\t\tnotFoundErr := NewResourceNotFoundError(nil, \"Resource not found\")\n\t\tif !IsNotFound(notFoundErr) {\n\t\t\tt.Error(\"IsNotFound should return true for not found errors\")\n\t\t}\n\t})\n}\n\nfunc TestErrorWrapping(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"WithSuggestions adds suggestions\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\toriginalErr := NewValidationError(nil, \"Invalid input\")\n\t\terrWithSuggestions := WithSuggestions(originalErr, \"Try this instead\", \"Or this\")\n\n\t\t// Verify that it's still a validation error\n\t\tif !IsValidationError(errWithSuggestions) {\n\t\t\tt.Error(\"Error category should be preserved when adding suggestions\")\n\t\t}\n\n\t\t// Verify suggestions were added\n\t\tcliErr, ok := errWithSuggestions.(*Error)\n\t\tif !ok {\n\t\t\tt.Fatal(\"WithSuggestions should return a *Error\")\n\t\t}\n\t\tif len(cliErr.Suggestions) != 2 {\n\t\t\tt.Errorf(\"Expected 2 suggestions, got %d\", len(cliErr.Suggestions))\n\t\t}\n\t})\n\n\tt.Run(\"WithDetails adds details\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\toriginalErr := NewAPIError(nil, \"API request failed\")\n\t\terrWithDetails := WithDetails(originalErr, \"Additional context\")\n\n\t\t// Verify that it's still an API error\n\t\tif !IsAPIError(errWithDetails) {\n\t\t\tt.Error(\"Error category should be preserved when adding details\")\n\t\t}\n\n\t\t// Verify details were added\n\t\tcliErr, ok := errWithDetails.(*Error)\n\t\tif !ok {\n\t\t\tt.Fatal(\"WithDetails should return a *Error\")\n\t\t}\n\t\tif !strings.Contains(cliErr.Details, \"Additional context\") {\n\t\t\tt.Errorf(\"Details should contain added context, got %q\", cliErr.Details)\n\t\t}\n\t})\n\n\tt.Run(\"Unwrap returns original error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\toriginalErr := fmt.Errorf(\"original error\")\n\t\twrappedErr := NewAPIError(originalErr, \"API request failed\")\n\n\t\tunwrappedErr := errors.Unwrap(wrappedErr)\n\t\tif unwrappedErr != originalErr {\n\t\t\tt.Errorf(\"Unwrap should return original error, got %v\", unwrappedErr)\n\t\t}\n\t})\n\n\tt.Run(\"Unwrap with nil original returns category\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\twrappedErr := NewAPIError(nil, \"API request failed\")\n\n\t\tunwrappedErr := errors.Unwrap(wrappedErr)\n\t\tif unwrappedErr != ErrAPI {\n\t\t\tt.Errorf(\"Unwrap should return category when original is nil, got %v\", unwrappedErr)\n\t\t}\n\t})\n\n\tt.Run(\"errors.Is works with original error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\toriginalErr := fmt.Errorf(\"not found: resource does not exist\")\n\t\twrappedErr := NewResourceNotFoundError(originalErr, \"Could not find resource\")\n\n\t\t// Should match both the category and the original error\n\t\tif !errors.Is(wrappedErr, ErrResourceNotFound) {\n\t\t\tt.Error(\"errors.Is should match error category\")\n\t\t}\n\t\tif !errors.Is(wrappedErr, originalErr) {\n\t\t\tt.Error(\"errors.Is should match original error\")\n\t\t}\n\t})\n}\n\nfunc TestErrorCreationHelpers(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tcreateFunc func(error, string, ...string) error\n\t\terrorType  error\n\t\tcheckFunc  func(error) bool\n\t}{\n\t\t{\n\t\t\tname:       \"NewConfigurationError\",\n\t\t\tcreateFunc: NewConfigurationError,\n\t\t\terrorType:  ErrConfiguration,\n\t\t\tcheckFunc:  IsConfigurationError,\n\t\t},\n\t\t{\n\t\t\tname:       \"NewValidationError\",\n\t\t\tcreateFunc: NewValidationError,\n\t\t\terrorType:  ErrValidation,\n\t\t\tcheckFunc:  IsValidationError,\n\t\t},\n\t\t{\n\t\t\tname:       \"NewAPIError\",\n\t\t\tcreateFunc: NewAPIError,\n\t\t\terrorType:  ErrAPI,\n\t\t\tcheckFunc:  IsAPIError,\n\t\t},\n\t\t{\n\t\t\tname:       \"NewResourceNotFoundError\",\n\t\t\tcreateFunc: NewResourceNotFoundError,\n\t\t\terrorType:  ErrResourceNotFound,\n\t\t\tcheckFunc:  IsNotFound,\n\t\t},\n\t\t{\n\t\t\tname:       \"NewPermissionDeniedError\",\n\t\t\tcreateFunc: NewPermissionDeniedError,\n\t\t\terrorType:  ErrPermissionDenied,\n\t\t\tcheckFunc:  IsPermissionDeniedError,\n\t\t},\n\t\t{\n\t\t\tname:       \"NewAuthenticationError\",\n\t\t\tcreateFunc: NewAuthenticationError,\n\t\t\terrorType:  ErrAuthentication,\n\t\t\tcheckFunc:  IsAuthenticationError,\n\t\t},\n\t\t{\n\t\t\tname:       \"NewUserAbortedError\",\n\t\t\tcreateFunc: NewUserAbortedError,\n\t\t\terrorType:  ErrUserAborted,\n\t\t\tcheckFunc:  IsUserAborted,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\ttc := tc // Capture test case value\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\toriginalErr := fmt.Errorf(\"some error\")\n\t\t\tdetails := \"detailed error message\"\n\t\t\tsuggestion := \"helpful suggestion\"\n\n\t\t\terr := tc.createFunc(originalErr, details, suggestion)\n\n\t\t\t// Check that the error has the right category\n\t\t\tif !errors.Is(err, tc.errorType) {\n\t\t\t\tt.Errorf(\"Error should be of type %v\", tc.errorType)\n\t\t\t}\n\n\t\t\t// Check that the error type check function works\n\t\t\tif !tc.checkFunc(err) {\n\t\t\t\tt.Errorf(\"Type check function should return true\")\n\t\t\t}\n\n\t\t\t// Check that details and suggestions are included\n\t\t\tcliErr, ok := err.(*Error)\n\t\t\tif !ok {\n\t\t\t\tt.Fatal(\"Error should be a *Error\")\n\t\t\t}\n\t\t\tif cliErr.Details != details {\n\t\t\t\tt.Errorf(\"Expected details %q, got %q\", details, cliErr.Details)\n\t\t\t}\n\t\t\tif len(cliErr.Suggestions) != 1 || cliErr.Suggestions[0] != suggestion {\n\t\t\t\tt.Errorf(\"Expected suggestion %q, got %v\", suggestion, cliErr.Suggestions)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/errors/handler.go",
    "content": "package errors\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n)\n\n// Exit codes for different error types\nconst (\n\tExitCodeSuccess = 0\n\n\tExitCodeGenericError = 1\n\n\tExitCodeValidationError            = 2\n\tExitCodeAPIError                   = 3\n\tExitCodeNotFoundError              = 4\n\tExitCodePermissionError            = 5\n\tExitCodeConfigError                = 6\n\tExitCodeAuthError                  = 7\n\tExitCodeInternalError              = 8\n\tExitCodePreflightCompletedFailure  = 9\n\tExitCodePreflightIncompleteFailure = 10\n\tExitCodePreflightIncomplete        = 11\n\tExitCodePreflightUnknown           = 12\n\tExitCodeUserAbortedError           = 130 // Same as Ctrl+C in bash\n)\n\n// Handler processes errors from commands and formats them appropriately\ntype Handler struct {\n\t// Writer is where error messages will be written\n\tWriter io.Writer\n\t// ExitFunc is the function called to exit the program with a specific code\n\tExitFunc func(int)\n\t// Verbose enables more detailed error messages\n\tVerbose bool\n}\n\n// NewHandler creates a new Handler with default settings\nfunc NewHandler() *Handler {\n\treturn &Handler{\n\t\tWriter:   os.Stderr,\n\t\tExitFunc: os.Exit,\n\t\tVerbose:  false,\n\t}\n}\n\n// WithWriter sets the writer for error output\nfunc (h *Handler) WithWriter(w io.Writer) *Handler {\n\th.Writer = w\n\treturn h\n}\n\n// WithExitFunc sets the exit function\nfunc (h *Handler) WithExitFunc(f func(int)) *Handler {\n\th.ExitFunc = f\n\treturn h\n}\n\n// WithVerbose sets the verbose flag\nfunc (h *Handler) WithVerbose(v bool) *Handler {\n\th.Verbose = v\n\treturn h\n}\n\n// Handle processes an error and outputs it appropriately\nfunc (h *Handler) Handle(err error) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\t// Get the exit code based on error type\n\texitCode := h.getExitCode(err)\n\n\t// Format the error message\n\tmessage := h.formatError(err)\n\n\t// Write the error message\n\tfmt.Fprintln(h.Writer, message)\n\n\t// Call the exit function with the appropriate code\n\tif h.ExitFunc != nil {\n\t\th.ExitFunc(exitCode)\n\t}\n}\n\n// getExitCode determines the appropriate exit code based on the error type\nfunc (h *Handler) getExitCode(err error) int {\n\tswitch {\n\tcase IsValidationError(err):\n\t\treturn ExitCodeValidationError\n\tcase IsAPIError(err):\n\t\treturn ExitCodeAPIError\n\tcase IsNotFound(err):\n\t\treturn ExitCodeNotFoundError\n\tcase IsPermissionDeniedError(err):\n\t\treturn ExitCodePermissionError\n\tcase IsConfigurationError(err):\n\t\treturn ExitCodeConfigError\n\tcase IsAuthenticationError(err):\n\t\treturn ExitCodeAuthError\n\tcase IsPreflightCompletedFailure(err):\n\t\treturn ExitCodePreflightCompletedFailure\n\tcase IsPreflightIncompleteFailure(err):\n\t\treturn ExitCodePreflightIncompleteFailure\n\tcase IsPreflightIncomplete(err):\n\t\treturn ExitCodePreflightIncomplete\n\tcase IsPreflightUnknown(err):\n\t\treturn ExitCodePreflightUnknown\n\tcase IsUserAborted(err):\n\t\treturn ExitCodeUserAbortedError\n\tcase errors.Is(err, ErrInternal):\n\t\treturn ExitCodeInternalError\n\tdefault:\n\t\treturn ExitCodeGenericError\n\t}\n}\n\n// formatError creates a formatted error message based on the error type\nfunc (h *Handler) formatError(err error) string {\n\tprefix := \"Error:\"\n\n\tif cliErr, ok := err.(*Error); ok {\n\t\t// For CLI errors, use the formatted error message\n\t\tvar message string\n\n\t\tif cliErr.Category != nil {\n\t\t\t// Get a more specific prefix based on the error category\n\t\t\tprefix = h.getCategoryPrefix(cliErr.Category)\n\t\t}\n\n\t\t// If verbose mode is enabled, include more details\n\t\tif h.Verbose {\n\t\t\tmessage = cliErr.FormattedError()\n\t\t} else {\n\t\t\t// In non-verbose mode, include the main error message and the first suggestion\n\t\t\tmessage = cliErr.Error()\n\t\t\tif len(cliErr.Suggestions) > 0 {\n\t\t\t\tmessage = fmt.Sprintf(\"%s\\nTip: %s\", message, cliErr.Suggestions[0])\n\t\t\t}\n\t\t}\n\n\t\treturn fmt.Sprintf(\"%s %s\", prefix, message)\n\t}\n\n\t// For regular errors, just return the error message\n\treturn fmt.Sprintf(\"%s %s\", prefix, err.Error())\n}\n\n// getCategoryPrefix returns an appropriate prefix for the error category\nfunc (h *Handler) getCategoryPrefix(category error) string {\n\tswitch category {\n\tcase ErrValidation:\n\t\treturn \"Validation Error:\"\n\tcase ErrAPI:\n\t\treturn \"API Error:\"\n\tcase ErrResourceNotFound:\n\t\treturn \"Not Found:\"\n\tcase ErrPermissionDenied:\n\t\treturn \"Permission Denied:\"\n\tcase ErrConfiguration:\n\t\treturn \"Configuration Error:\"\n\tcase ErrAuthentication:\n\t\treturn \"Authentication Error:\"\n\tcase ErrPreflightCompletedFailure:\n\t\treturn \"Preflight Failure:\"\n\tcase ErrPreflightIncompleteFailure:\n\t\treturn \"Preflight Failing:\"\n\tcase ErrPreflightIncomplete:\n\t\treturn \"Preflight Incomplete:\"\n\tcase ErrPreflightUnknown:\n\t\treturn \"Preflight Unknown Result:\"\n\tcase ErrUserAborted:\n\t\treturn \"Aborted:\"\n\tcase ErrInternal:\n\t\treturn \"Internal Error:\"\n\tdefault:\n\t\treturn \"Error:\"\n\t}\n}\n\n// HandleWithDetails processes an error with additional contextual details\nfunc (h *Handler) HandleWithDetails(err error, operation string) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\t// Add operation context to the error\n\tvar contextualErr error\n\tif operation != \"\" {\n\t\t// Check if it's already a CLI error\n\t\tif cliErr, ok := err.(*Error); ok {\n\t\t\t// Create a deep copy of the original error to avoid modifying it\n\t\t\tnewSuggestions := make([]string, len(cliErr.Suggestions))\n\t\t\tcopy(newSuggestions, cliErr.Suggestions)\n\n\t\t\tnewCliErr := &Error{\n\t\t\t\tOriginal:    cliErr.Original,\n\t\t\t\tCategory:    cliErr.Category,\n\t\t\t\tSuggestions: newSuggestions,\n\t\t\t\tDetails:     cliErr.Details,\n\t\t\t}\n\n\t\t\t// Add operation to details\n\t\t\tif newCliErr.Details == \"\" {\n\t\t\t\tnewCliErr.Details = fmt.Sprintf(\"failed during: %s\", operation)\n\t\t\t} else {\n\t\t\t\tnewCliErr.Details = fmt.Sprintf(\"%s (during: %s)\", newCliErr.Details, operation)\n\t\t\t}\n\t\t\tcontextualErr = newCliErr\n\t\t} else {\n\t\t\t// Wrap in a new error with operation details\n\t\t\tcontextualErr = NewError(err, nil, fmt.Sprintf(\"failed during: %s\", operation))\n\t\t}\n\t} else {\n\t\tcontextualErr = err\n\t}\n\n\t// Handle the contextual error\n\th.Handle(contextualErr)\n}\n\n// PrintWarning prints a warning message\nfunc (h *Handler) PrintWarning(format string, args ...interface{}) {\n\tmessage := fmt.Sprintf(format, args...)\n\tfmt.Fprintf(h.Writer, \"Warning: %s\\n\", message)\n}\n\n// MessageForError returns a formatted message for an error without exiting\nfunc MessageForError(err error) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\n\thandler := NewHandler()\n\treturn handler.formatError(err)\n}\n\n// GetExitCodeForError returns the exit code for a given error\nfunc GetExitCodeForError(err error) int {\n\tif err == nil {\n\t\treturn ExitCodeSuccess\n\t}\n\n\thandler := NewHandler()\n\treturn handler.getExitCode(err)\n}\n"
  },
  {
    "path": "internal/errors/handler_test.go",
    "content": "package errors\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n)\n\n// stripANSI removes ANSI color codes from a string for easier testing\nfunc stripANSI(s string) string {\n\tr := strings.NewReplacer(\n\t\t\"\\x1b[0m\", \"\",\n\t\t\"\\x1b[1m\", \"\",\n\t\t\"\\x1b[2m\", \"\",\n\t\t\"\\x1b[31m\", \"\",\n\t\t\"\\x1b[32m\", \"\",\n\t\t\"\\x1b[33m\", \"\",\n\t\t\"\\x1b[34m\", \"\",\n\t\t\"\\x1b[35m\", \"\",\n\t\t\"\\x1b[36m\", \"\",\n\t\t\"\\x1b[37m\", \"\",\n\t\t\"\\x1b[91m\", \"\",\n\t\t\"\\x1b[92m\", \"\",\n\t\t\"\\x1b[93m\", \"\",\n\t\t\"\\x1b[94m\", \"\",\n\t\t\"\\x1b[95m\", \"\",\n\t\t\"\\x1b[96m\", \"\",\n\t\t\"\\x1b[97m\", \"\",\n\t)\n\treturn r.Replace(s)\n}\n\nfunc TestHandler(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"handles nil error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar buf bytes.Buffer\n\t\tvar exitCode int\n\n\t\thandler := NewHandler().\n\t\t\tWithWriter(&buf).\n\t\t\tWithExitFunc(func(code int) { exitCode = code })\n\n\t\thandler.Handle(nil)\n\n\t\tif buf.Len() > 0 {\n\t\t\tt.Errorf(\"Expected no output for nil error, got: %q\", buf.String())\n\t\t}\n\t\tif exitCode != 0 {\n\t\t\tt.Errorf(\"Expected exit code 0 for nil error, got: %d\", exitCode)\n\t\t}\n\t})\n\n\tt.Run(\"formats different error types\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestCases := []struct {\n\t\t\tname           string\n\t\t\terr            error\n\t\t\texpectedPrefix string\n\t\t\texpectedCode   int\n\t\t}{\n\t\t\t{\n\t\t\t\tname:           \"validation error\",\n\t\t\t\terr:            NewValidationError(nil, \"Invalid input\"),\n\t\t\t\texpectedPrefix: \"Validation Error:\",\n\t\t\t\texpectedCode:   ExitCodeValidationError,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:           \"API error\",\n\t\t\t\terr:            NewAPIError(nil, \"API request failed\"),\n\t\t\t\texpectedPrefix: \"API Error:\",\n\t\t\t\texpectedCode:   ExitCodeAPIError,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:           \"not found error\",\n\t\t\t\terr:            NewResourceNotFoundError(nil, \"Resource not found\"),\n\t\t\t\texpectedPrefix: \"Not Found:\",\n\t\t\t\texpectedCode:   ExitCodeNotFoundError,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:           \"simple error\",\n\t\t\t\terr:            fmt.Errorf(\"simple error\"),\n\t\t\t\texpectedPrefix: \"Error:\",\n\t\t\t\texpectedCode:   ExitCodeGenericError,\n\t\t\t},\n\t\t}\n\n\t\tfor _, tc := range testCases {\n\t\t\ttc := tc // Capture range variable\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\tvar buf bytes.Buffer\n\t\t\t\tvar exitCode int\n\n\t\t\t\thandler := NewHandler().\n\t\t\t\t\tWithWriter(&buf).\n\t\t\t\t\tWithExitFunc(func(code int) { exitCode = code })\n\n\t\t\t\thandler.Handle(tc.err)\n\n\t\t\t\toutput := stripANSI(buf.String())\n\t\t\t\tif !strings.Contains(output, tc.expectedPrefix) {\n\t\t\t\t\tt.Errorf(\"Expected output to contain %q, got: %q\", tc.expectedPrefix, output)\n\t\t\t\t}\n\n\t\t\t\tif exitCode != tc.expectedCode {\n\t\t\t\t\tt.Errorf(\"Expected exit code %d, got: %d\", tc.expectedCode, exitCode)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"includes suggestions in verbose mode\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar buf bytes.Buffer\n\t\thandler := NewHandler().\n\t\t\tWithWriter(&buf).\n\t\t\tWithExitFunc(func(int) {}).\n\t\t\tWithVerbose(true)\n\n\t\tsuggestion1 := \"Try using a different name\"\n\t\tsuggestion2 := \"Check your spelling\"\n\t\terr := NewValidationError(nil, \"Invalid name\", suggestion1, suggestion2)\n\n\t\thandler.Handle(err)\n\n\t\toutput := stripANSI(buf.String())\n\t\tif !strings.Contains(output, suggestion1) {\n\t\t\tt.Errorf(\"Expected output to contain suggestion %q, got: %q\", suggestion1, output)\n\t\t}\n\t\tif !strings.Contains(output, suggestion2) {\n\t\t\tt.Errorf(\"Expected output to contain suggestion %q, got: %q\", suggestion2, output)\n\t\t}\n\t})\n\n\tt.Run(\"includes one suggestion in non-verbose mode\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar buf bytes.Buffer\n\t\thandler := NewHandler().\n\t\t\tWithWriter(&buf).\n\t\t\tWithExitFunc(func(int) {})\n\n\t\tsuggestion1 := \"Try using a different name\"\n\t\tsuggestion2 := \"Check your spelling\"\n\t\terr := NewValidationError(nil, \"Invalid name\", suggestion1, suggestion2)\n\n\t\thandler.Handle(err)\n\n\t\toutput := stripANSI(buf.String())\n\t\tif !strings.Contains(output, suggestion1) {\n\t\t\tt.Errorf(\"Expected output to contain first suggestion %q, got: %q\", suggestion1, output)\n\t\t}\n\t\tif strings.Contains(output, suggestion2) {\n\t\t\tt.Errorf(\"Expected output to NOT contain second suggestion in non-verbose mode, got: %q\", output)\n\t\t}\n\t})\n\n\tt.Run(\"handles errors with details\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar buf bytes.Buffer\n\t\thandler := NewHandler().\n\t\t\tWithWriter(&buf).\n\t\t\tWithExitFunc(func(int) {})\n\n\t\t// Create a simple error that won't be shared with other tests\n\t\terr := fmt.Errorf(\"test error: something went wrong\")\n\t\toperation := \"fetching data\"\n\n\t\thandler.HandleWithDetails(err, operation)\n\n\t\toutput := stripANSI(buf.String())\n\t\tif !strings.Contains(output, \"something went wrong\") {\n\t\t\tt.Errorf(\"Expected output to contain error message, got: %q\", output)\n\t\t}\n\t\tif !strings.Contains(output, operation) {\n\t\t\tt.Errorf(\"Expected output to contain operation details %q, got: %q\", operation, output)\n\t\t}\n\t})\n\n\tt.Run(\"prints warnings\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar buf bytes.Buffer\n\t\thandler := NewHandler().\n\t\t\tWithWriter(&buf)\n\n\t\twarningMsg := \"Something might be wrong\"\n\t\thandler.PrintWarning(\"%s\", warningMsg)\n\n\t\toutput := stripANSI(buf.String())\n\t\tif !strings.Contains(output, \"Warning:\") {\n\t\t\tt.Errorf(\"Expected output to contain 'Warning:', got: %q\", output)\n\t\t}\n\t\tif !strings.Contains(output, warningMsg) {\n\t\t\tt.Errorf(\"Expected output to contain warning message %q, got: %q\", warningMsg, output)\n\t\t}\n\t})\n\n\tt.Run(\"MessageForError returns formatted message\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\terr := NewValidationError(nil, \"Invalid input\")\n\t\tmessage := MessageForError(err)\n\n\t\t// Strip ANSI codes for testing\n\t\tplainMessage := stripANSI(message)\n\n\t\tif !strings.Contains(plainMessage, \"Validation Error:\") {\n\t\t\tt.Errorf(\"Expected message to contain error category, got: %q\", plainMessage)\n\t\t}\n\t\tif !strings.Contains(plainMessage, \"Invalid input\") {\n\t\t\tt.Errorf(\"Expected message to contain error details, got: %q\", plainMessage)\n\t\t}\n\t})\n\n\tt.Run(\"GetExitCodeForError returns correct code\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestCases := []struct {\n\t\t\tname         string\n\t\t\terr          error\n\t\t\texpectedCode int\n\t\t}{\n\t\t\t{\n\t\t\t\tname:         \"nil error\",\n\t\t\t\terr:          nil,\n\t\t\t\texpectedCode: ExitCodeSuccess,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:         \"validation error\",\n\t\t\t\terr:          NewValidationError(nil, \"\"),\n\t\t\t\texpectedCode: ExitCodeValidationError,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:         \"API error\",\n\t\t\t\terr:          NewAPIError(nil, \"\"),\n\t\t\t\texpectedCode: ExitCodeAPIError,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:         \"not found error\",\n\t\t\t\terr:          NewResourceNotFoundError(nil, \"\"),\n\t\t\t\texpectedCode: ExitCodeNotFoundError,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:         \"preflight completed failure\",\n\t\t\t\terr:          NewPreflightCompletedFailureError(fmt.Errorf(\"failed\"), \"\"),\n\t\t\t\texpectedCode: ExitCodePreflightCompletedFailure,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:         \"preflight incomplete failure\",\n\t\t\t\terr:          NewPreflightIncompleteFailureError(fmt.Errorf(\"failed\"), \"\"),\n\t\t\t\texpectedCode: ExitCodePreflightIncompleteFailure,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:         \"preflight incomplete\",\n\t\t\t\terr:          NewPreflightIncompleteError(fmt.Errorf(\"incomplete\"), \"\"),\n\t\t\t\texpectedCode: ExitCodePreflightIncomplete,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:         \"preflight unknown result\",\n\t\t\t\terr:          NewPreflightUnknownError(fmt.Errorf(\"unknown\"), \"\"),\n\t\t\t\texpectedCode: ExitCodePreflightUnknown,\n\t\t\t},\n\t\t}\n\n\t\tfor _, tc := range testCases {\n\t\t\ttc := tc\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\t\t\t\tcode := GetExitCodeForError(tc.err)\n\t\t\t\tif code != tc.expectedCode {\n\t\t\t\t\tt.Errorf(\"Expected exit code %d, got: %d\", tc.expectedCode, code)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/graphql/generated.go",
    "content": "// Code generated by github.com/Khan/genqlient, DO NOT EDIT.\n\npackage graphql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/Khan/genqlient/graphql\"\n)\n\n// CancelJobJobTypeCommandCancelJobTypeCommandCancelPayload includes the requested fields of the GraphQL type JobTypeCommandCancelPayload.\n// The GraphQL type's documentation follows.\n//\n// Autogenerated return type of JobTypeCommandCancel.\ntype CancelJobJobTypeCommandCancelJobTypeCommandCancelPayload struct {\n\t// A unique identifier for the client performing the mutation.\n\tClientMutationId *string                                                                `json:\"clientMutationId\"`\n\tJobTypeCommand   CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand `json:\"jobTypeCommand\"`\n}\n\n// GetClientMutationId returns CancelJobJobTypeCommandCancelJobTypeCommandCancelPayload.ClientMutationId, and is useful for accessing the field via an interface.\nfunc (v *CancelJobJobTypeCommandCancelJobTypeCommandCancelPayload) GetClientMutationId() *string {\n\treturn v.ClientMutationId\n}\n\n// GetJobTypeCommand returns CancelJobJobTypeCommandCancelJobTypeCommandCancelPayload.JobTypeCommand, and is useful for accessing the field via an interface.\nfunc (v *CancelJobJobTypeCommandCancelJobTypeCommandCancelPayload) GetJobTypeCommand() CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand {\n\treturn v.JobTypeCommand\n}\n\n// CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand includes the requested fields of the GraphQL type JobTypeCommand.\n// The GraphQL type's documentation follows.\n//\n// A type of job that runs a command on an agent\ntype CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand struct {\n\tId string `json:\"id\"`\n\t// The UUID for this job\n\tUuid string `json:\"uuid\"`\n\t// The state of the job\n\tState JobStates `json:\"state\"`\n\t// The URL for the job\n\tUrl string `json:\"url\"`\n}\n\n// GetId returns CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand.Id, and is useful for accessing the field via an interface.\nfunc (v *CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand) GetId() string {\n\treturn v.Id\n}\n\n// GetUuid returns CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand.Uuid, and is useful for accessing the field via an interface.\nfunc (v *CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand) GetUuid() string {\n\treturn v.Uuid\n}\n\n// GetState returns CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand.State, and is useful for accessing the field via an interface.\nfunc (v *CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand) GetState() JobStates {\n\treturn v.State\n}\n\n// GetUrl returns CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand.Url, and is useful for accessing the field via an interface.\nfunc (v *CancelJobJobTypeCommandCancelJobTypeCommandCancelPayloadJobTypeCommand) GetUrl() string {\n\treturn v.Url\n}\n\n// CancelJobResponse is returned by CancelJob on success.\ntype CancelJobResponse struct {\n\t// Cancel a job.\n\tJobTypeCommandCancel *CancelJobJobTypeCommandCancelJobTypeCommandCancelPayload `json:\"jobTypeCommandCancel\"`\n}\n\n// GetJobTypeCommandCancel returns CancelJobResponse.JobTypeCommandCancel, and is useful for accessing the field via an interface.\nfunc (v *CancelJobResponse) GetJobTypeCommandCancel() *CancelJobJobTypeCommandCancelJobTypeCommandCancelPayload {\n\treturn v.JobTypeCommandCancel\n}\n\n// FindClustersOrganization includes the requested fields of the GraphQL type Organization.\n// The GraphQL type's documentation follows.\n//\n// An organization\ntype FindClustersOrganization struct {\n\t// Returns clusters for an Organization\n\tClusters *FindClustersOrganizationClustersClusterConnection `json:\"clusters\"`\n}\n\n// GetClusters returns FindClustersOrganization.Clusters, and is useful for accessing the field via an interface.\nfunc (v *FindClustersOrganization) GetClusters() *FindClustersOrganizationClustersClusterConnection {\n\treturn v.Clusters\n}\n\n// FindClustersOrganizationClustersClusterConnection includes the requested fields of the GraphQL type ClusterConnection.\ntype FindClustersOrganizationClustersClusterConnection struct {\n\tEdges    []*FindClustersOrganizationClustersClusterConnectionEdgesClusterEdge `json:\"edges\"`\n\tPageInfo *FindClustersOrganizationClustersClusterConnectionPageInfo           `json:\"pageInfo\"`\n}\n\n// GetEdges returns FindClustersOrganizationClustersClusterConnection.Edges, and is useful for accessing the field via an interface.\nfunc (v *FindClustersOrganizationClustersClusterConnection) GetEdges() []*FindClustersOrganizationClustersClusterConnectionEdgesClusterEdge {\n\treturn v.Edges\n}\n\n// GetPageInfo returns FindClustersOrganizationClustersClusterConnection.PageInfo, and is useful for accessing the field via an interface.\nfunc (v *FindClustersOrganizationClustersClusterConnection) GetPageInfo() *FindClustersOrganizationClustersClusterConnectionPageInfo {\n\treturn v.PageInfo\n}\n\n// FindClustersOrganizationClustersClusterConnectionEdgesClusterEdge includes the requested fields of the GraphQL type ClusterEdge.\ntype FindClustersOrganizationClustersClusterConnectionEdgesClusterEdge struct {\n\tNode *FindClustersOrganizationClustersClusterConnectionEdgesClusterEdgeNodeCluster `json:\"node\"`\n}\n\n// GetNode returns FindClustersOrganizationClustersClusterConnectionEdgesClusterEdge.Node, and is useful for accessing the field via an interface.\nfunc (v *FindClustersOrganizationClustersClusterConnectionEdgesClusterEdge) GetNode() *FindClustersOrganizationClustersClusterConnectionEdgesClusterEdgeNodeCluster {\n\treturn v.Node\n}\n\n// FindClustersOrganizationClustersClusterConnectionEdgesClusterEdgeNodeCluster includes the requested fields of the GraphQL type Cluster.\ntype FindClustersOrganizationClustersClusterConnectionEdgesClusterEdgeNodeCluster struct {\n\tId string `json:\"id\"`\n\t// Name of the cluster\n\tName string `json:\"name\"`\n}\n\n// GetId returns FindClustersOrganizationClustersClusterConnectionEdgesClusterEdgeNodeCluster.Id, and is useful for accessing the field via an interface.\nfunc (v *FindClustersOrganizationClustersClusterConnectionEdgesClusterEdgeNodeCluster) GetId() string {\n\treturn v.Id\n}\n\n// GetName returns FindClustersOrganizationClustersClusterConnectionEdgesClusterEdgeNodeCluster.Name, and is useful for accessing the field via an interface.\nfunc (v *FindClustersOrganizationClustersClusterConnectionEdgesClusterEdgeNodeCluster) GetName() string {\n\treturn v.Name\n}\n\n// FindClustersOrganizationClustersClusterConnectionPageInfo includes the requested fields of the GraphQL type PageInfo.\n// The GraphQL type's documentation follows.\n//\n// Information about pagination in a connection.\ntype FindClustersOrganizationClustersClusterConnectionPageInfo struct {\n\t// When paginating forwards, are there more items?\n\tHasNextPage bool `json:\"hasNextPage\"`\n\t// When paginating forwards, the cursor to continue.\n\tEndCursor *string `json:\"endCursor\"`\n}\n\n// GetHasNextPage returns FindClustersOrganizationClustersClusterConnectionPageInfo.HasNextPage, and is useful for accessing the field via an interface.\nfunc (v *FindClustersOrganizationClustersClusterConnectionPageInfo) GetHasNextPage() bool {\n\treturn v.HasNextPage\n}\n\n// GetEndCursor returns FindClustersOrganizationClustersClusterConnectionPageInfo.EndCursor, and is useful for accessing the field via an interface.\nfunc (v *FindClustersOrganizationClustersClusterConnectionPageInfo) GetEndCursor() *string {\n\treturn v.EndCursor\n}\n\n// FindClustersResponse is returned by FindClusters on success.\ntype FindClustersResponse struct {\n\t// Find an organization\n\tOrganization *FindClustersOrganization `json:\"organization\"`\n}\n\n// GetOrganization returns FindClustersResponse.Organization, and is useful for accessing the field via an interface.\nfunc (v *FindClustersResponse) GetOrganization() *FindClustersOrganization { return v.Organization }\n\n// FindQueuesForClusterNode includes the requested fields of the GraphQL interface Node.\n//\n// FindQueuesForClusterNode is implemented by the following types:\n// FindQueuesForClusterNodeAPIAccessToken\n// FindQueuesForClusterNodeAPIAccessTokenCode\n// FindQueuesForClusterNodeAPIApplication\n// FindQueuesForClusterNodeAgent\n// FindQueuesForClusterNodeAgentToken\n// FindQueuesForClusterNodeAnnotation\n// FindQueuesForClusterNodeArtifact\n// FindQueuesForClusterNodeAuditEvent\n// FindQueuesForClusterNodeAuthorizationBitbucket\n// FindQueuesForClusterNodeAuthorizationGitHub\n// FindQueuesForClusterNodeAuthorizationGitHubApp\n// FindQueuesForClusterNodeAuthorizationGitHubEnterprise\n// FindQueuesForClusterNodeAuthorizationGoogle\n// FindQueuesForClusterNodeAuthorizationSAML\n// FindQueuesForClusterNodeBuild\n// FindQueuesForClusterNodeChangelog\n// FindQueuesForClusterNodeCluster\n// FindQueuesForClusterNodeClusterQueue\n// FindQueuesForClusterNodeClusterQueueToken\n// FindQueuesForClusterNodeClusterToken\n// FindQueuesForClusterNodeEmail\n// FindQueuesForClusterNodeJobEventAssigned\n// FindQueuesForClusterNodeJobEventBuildStepUploadCreated\n// FindQueuesForClusterNodeJobEventCanceled\n// FindQueuesForClusterNodeJobEventFinished\n// FindQueuesForClusterNodeJobEventGeneric\n// FindQueuesForClusterNodeJobEventRetried\n// FindQueuesForClusterNodeJobEventTimedOut\n// FindQueuesForClusterNodeJobTypeBlock\n// FindQueuesForClusterNodeJobTypeCommand\n// FindQueuesForClusterNodeJobTypeTrigger\n// FindQueuesForClusterNodeJobTypeWait\n// FindQueuesForClusterNodeNotificationServiceSlack\n// FindQueuesForClusterNodeOrganization\n// FindQueuesForClusterNodeOrganizationBanner\n// FindQueuesForClusterNodeOrganizationInvitation\n// FindQueuesForClusterNodeOrganizationMember\n// FindQueuesForClusterNodePipeline\n// FindQueuesForClusterNodePipelineMetric\n// FindQueuesForClusterNodePipelineSchedule\n// FindQueuesForClusterNodePipelineTemplate\n// FindQueuesForClusterNodeSSOProviderGitHubApp\n// FindQueuesForClusterNodeSSOProviderGoogleGSuite\n// FindQueuesForClusterNodeSSOProviderSAML\n// FindQueuesForClusterNodeSecret\n// FindQueuesForClusterNodeSuite\n// FindQueuesForClusterNodeTeam\n// FindQueuesForClusterNodeTeamMember\n// FindQueuesForClusterNodeTeamPipeline\n// FindQueuesForClusterNodeTeamSuite\n// FindQueuesForClusterNodeUser\n// FindQueuesForClusterNodeViewer\n// The GraphQL type's documentation follows.\n//\n// An object with an ID.\ntype FindQueuesForClusterNode interface {\n\timplementsGraphQLInterfaceFindQueuesForClusterNode()\n\t// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).\n\tGetTypename() *string\n}\n\nfunc (v *FindQueuesForClusterNodeAPIAccessToken) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeAPIAccessTokenCode) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeAPIApplication) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeAgent) implementsGraphQLInterfaceFindQueuesForClusterNode()      {}\nfunc (v *FindQueuesForClusterNodeAgentToken) implementsGraphQLInterfaceFindQueuesForClusterNode() {}\nfunc (v *FindQueuesForClusterNodeAnnotation) implementsGraphQLInterfaceFindQueuesForClusterNode() {}\nfunc (v *FindQueuesForClusterNodeArtifact) implementsGraphQLInterfaceFindQueuesForClusterNode()   {}\nfunc (v *FindQueuesForClusterNodeAuditEvent) implementsGraphQLInterfaceFindQueuesForClusterNode() {}\nfunc (v *FindQueuesForClusterNodeAuthorizationBitbucket) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeAuthorizationGitHub) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeAuthorizationGitHubApp) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeAuthorizationGitHubEnterprise) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeAuthorizationGoogle) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeAuthorizationSAML) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeBuild) implementsGraphQLInterfaceFindQueuesForClusterNode()        {}\nfunc (v *FindQueuesForClusterNodeChangelog) implementsGraphQLInterfaceFindQueuesForClusterNode()    {}\nfunc (v *FindQueuesForClusterNodeCluster) implementsGraphQLInterfaceFindQueuesForClusterNode()      {}\nfunc (v *FindQueuesForClusterNodeClusterQueue) implementsGraphQLInterfaceFindQueuesForClusterNode() {}\nfunc (v *FindQueuesForClusterNodeClusterQueueToken) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeClusterToken) implementsGraphQLInterfaceFindQueuesForClusterNode() {}\nfunc (v *FindQueuesForClusterNodeEmail) implementsGraphQLInterfaceFindQueuesForClusterNode()        {}\nfunc (v *FindQueuesForClusterNodeJobEventAssigned) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeJobEventBuildStepUploadCreated) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeJobEventCanceled) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeJobEventFinished) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeJobEventGeneric) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeJobEventRetried) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeJobEventTimedOut) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeJobTypeBlock) implementsGraphQLInterfaceFindQueuesForClusterNode() {}\nfunc (v *FindQueuesForClusterNodeJobTypeCommand) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeJobTypeTrigger) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeJobTypeWait) implementsGraphQLInterfaceFindQueuesForClusterNode() {}\nfunc (v *FindQueuesForClusterNodeNotificationServiceSlack) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeOrganization) implementsGraphQLInterfaceFindQueuesForClusterNode() {}\nfunc (v *FindQueuesForClusterNodeOrganizationBanner) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeOrganizationInvitation) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeOrganizationMember) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodePipeline) implementsGraphQLInterfaceFindQueuesForClusterNode() {}\nfunc (v *FindQueuesForClusterNodePipelineMetric) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodePipelineSchedule) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodePipelineTemplate) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeSSOProviderGitHubApp) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeSSOProviderGoogleGSuite) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeSSOProviderSAML) implementsGraphQLInterfaceFindQueuesForClusterNode() {\n}\nfunc (v *FindQueuesForClusterNodeSecret) implementsGraphQLInterfaceFindQueuesForClusterNode()       {}\nfunc (v *FindQueuesForClusterNodeSuite) implementsGraphQLInterfaceFindQueuesForClusterNode()        {}\nfunc (v *FindQueuesForClusterNodeTeam) implementsGraphQLInterfaceFindQueuesForClusterNode()         {}\nfunc (v *FindQueuesForClusterNodeTeamMember) implementsGraphQLInterfaceFindQueuesForClusterNode()   {}\nfunc (v *FindQueuesForClusterNodeTeamPipeline) implementsGraphQLInterfaceFindQueuesForClusterNode() {}\nfunc (v *FindQueuesForClusterNodeTeamSuite) implementsGraphQLInterfaceFindQueuesForClusterNode()    {}\nfunc (v *FindQueuesForClusterNodeUser) implementsGraphQLInterfaceFindQueuesForClusterNode()         {}\nfunc (v *FindQueuesForClusterNodeViewer) implementsGraphQLInterfaceFindQueuesForClusterNode()       {}\n\nfunc __unmarshalFindQueuesForClusterNode(b []byte, v *FindQueuesForClusterNode) error {\n\tif string(b) == \"null\" {\n\t\treturn nil\n\t}\n\n\tvar tn struct {\n\t\tTypeName string `json:\"__typename\"`\n\t}\n\terr := json.Unmarshal(b, &tn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch tn.TypeName {\n\tcase \"APIAccessToken\":\n\t\t*v = new(FindQueuesForClusterNodeAPIAccessToken)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"APIAccessTokenCode\":\n\t\t*v = new(FindQueuesForClusterNodeAPIAccessTokenCode)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"APIApplication\":\n\t\t*v = new(FindQueuesForClusterNodeAPIApplication)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Agent\":\n\t\t*v = new(FindQueuesForClusterNodeAgent)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"AgentToken\":\n\t\t*v = new(FindQueuesForClusterNodeAgentToken)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Annotation\":\n\t\t*v = new(FindQueuesForClusterNodeAnnotation)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Artifact\":\n\t\t*v = new(FindQueuesForClusterNodeArtifact)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"AuditEvent\":\n\t\t*v = new(FindQueuesForClusterNodeAuditEvent)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"AuthorizationBitbucket\":\n\t\t*v = new(FindQueuesForClusterNodeAuthorizationBitbucket)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"AuthorizationGitHub\":\n\t\t*v = new(FindQueuesForClusterNodeAuthorizationGitHub)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"AuthorizationGitHubApp\":\n\t\t*v = new(FindQueuesForClusterNodeAuthorizationGitHubApp)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"AuthorizationGitHubEnterprise\":\n\t\t*v = new(FindQueuesForClusterNodeAuthorizationGitHubEnterprise)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"AuthorizationGoogle\":\n\t\t*v = new(FindQueuesForClusterNodeAuthorizationGoogle)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"AuthorizationSAML\":\n\t\t*v = new(FindQueuesForClusterNodeAuthorizationSAML)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Build\":\n\t\t*v = new(FindQueuesForClusterNodeBuild)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Changelog\":\n\t\t*v = new(FindQueuesForClusterNodeChangelog)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Cluster\":\n\t\t*v = new(FindQueuesForClusterNodeCluster)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"ClusterQueue\":\n\t\t*v = new(FindQueuesForClusterNodeClusterQueue)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"ClusterQueueToken\":\n\t\t*v = new(FindQueuesForClusterNodeClusterQueueToken)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"ClusterToken\":\n\t\t*v = new(FindQueuesForClusterNodeClusterToken)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Email\":\n\t\t*v = new(FindQueuesForClusterNodeEmail)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobEventAssigned\":\n\t\t*v = new(FindQueuesForClusterNodeJobEventAssigned)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobEventBuildStepUploadCreated\":\n\t\t*v = new(FindQueuesForClusterNodeJobEventBuildStepUploadCreated)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobEventCanceled\":\n\t\t*v = new(FindQueuesForClusterNodeJobEventCanceled)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobEventFinished\":\n\t\t*v = new(FindQueuesForClusterNodeJobEventFinished)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobEventGeneric\":\n\t\t*v = new(FindQueuesForClusterNodeJobEventGeneric)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobEventRetried\":\n\t\t*v = new(FindQueuesForClusterNodeJobEventRetried)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobEventTimedOut\":\n\t\t*v = new(FindQueuesForClusterNodeJobEventTimedOut)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeBlock\":\n\t\t*v = new(FindQueuesForClusterNodeJobTypeBlock)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeCommand\":\n\t\t*v = new(FindQueuesForClusterNodeJobTypeCommand)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeTrigger\":\n\t\t*v = new(FindQueuesForClusterNodeJobTypeTrigger)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeWait\":\n\t\t*v = new(FindQueuesForClusterNodeJobTypeWait)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"NotificationServiceSlack\":\n\t\t*v = new(FindQueuesForClusterNodeNotificationServiceSlack)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Organization\":\n\t\t*v = new(FindQueuesForClusterNodeOrganization)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"OrganizationBanner\":\n\t\t*v = new(FindQueuesForClusterNodeOrganizationBanner)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"OrganizationInvitation\":\n\t\t*v = new(FindQueuesForClusterNodeOrganizationInvitation)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"OrganizationMember\":\n\t\t*v = new(FindQueuesForClusterNodeOrganizationMember)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Pipeline\":\n\t\t*v = new(FindQueuesForClusterNodePipeline)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"PipelineMetric\":\n\t\t*v = new(FindQueuesForClusterNodePipelineMetric)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"PipelineSchedule\":\n\t\t*v = new(FindQueuesForClusterNodePipelineSchedule)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"PipelineTemplate\":\n\t\t*v = new(FindQueuesForClusterNodePipelineTemplate)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"SSOProviderGitHubApp\":\n\t\t*v = new(FindQueuesForClusterNodeSSOProviderGitHubApp)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"SSOProviderGoogleGSuite\":\n\t\t*v = new(FindQueuesForClusterNodeSSOProviderGoogleGSuite)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"SSOProviderSAML\":\n\t\t*v = new(FindQueuesForClusterNodeSSOProviderSAML)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Secret\":\n\t\t*v = new(FindQueuesForClusterNodeSecret)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Suite\":\n\t\t*v = new(FindQueuesForClusterNodeSuite)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Team\":\n\t\t*v = new(FindQueuesForClusterNodeTeam)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"TeamMember\":\n\t\t*v = new(FindQueuesForClusterNodeTeamMember)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"TeamPipeline\":\n\t\t*v = new(FindQueuesForClusterNodeTeamPipeline)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"TeamSuite\":\n\t\t*v = new(FindQueuesForClusterNodeTeamSuite)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"User\":\n\t\t*v = new(FindQueuesForClusterNodeUser)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"Viewer\":\n\t\t*v = new(FindQueuesForClusterNodeViewer)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"\":\n\t\treturn fmt.Errorf(\n\t\t\t\"response was missing Node.__typename\")\n\tdefault:\n\t\treturn fmt.Errorf(\n\t\t\t`unexpected concrete type for FindQueuesForClusterNode: \"%v\"`, tn.TypeName)\n\t}\n}\n\nfunc __marshalFindQueuesForClusterNode(v *FindQueuesForClusterNode) ([]byte, error) {\n\n\tvar typename string\n\tswitch v := (*v).(type) {\n\tcase *FindQueuesForClusterNodeAPIAccessToken:\n\t\ttypename = \"APIAccessToken\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAPIAccessToken\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAPIAccessTokenCode:\n\t\ttypename = \"APIAccessTokenCode\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAPIAccessTokenCode\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAPIApplication:\n\t\ttypename = \"APIApplication\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAPIApplication\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAgent:\n\t\ttypename = \"Agent\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAgent\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAgentToken:\n\t\ttypename = \"AgentToken\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAgentToken\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAnnotation:\n\t\ttypename = \"Annotation\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAnnotation\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeArtifact:\n\t\ttypename = \"Artifact\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeArtifact\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAuditEvent:\n\t\ttypename = \"AuditEvent\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAuditEvent\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAuthorizationBitbucket:\n\t\ttypename = \"AuthorizationBitbucket\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAuthorizationBitbucket\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAuthorizationGitHub:\n\t\ttypename = \"AuthorizationGitHub\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAuthorizationGitHub\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAuthorizationGitHubApp:\n\t\ttypename = \"AuthorizationGitHubApp\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAuthorizationGitHubApp\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAuthorizationGitHubEnterprise:\n\t\ttypename = \"AuthorizationGitHubEnterprise\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAuthorizationGitHubEnterprise\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAuthorizationGoogle:\n\t\ttypename = \"AuthorizationGoogle\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAuthorizationGoogle\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeAuthorizationSAML:\n\t\ttypename = \"AuthorizationSAML\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeAuthorizationSAML\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeBuild:\n\t\ttypename = \"Build\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeBuild\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeChangelog:\n\t\ttypename = \"Changelog\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeChangelog\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeCluster:\n\t\ttypename = \"Cluster\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeCluster\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeClusterQueue:\n\t\ttypename = \"ClusterQueue\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeClusterQueue\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeClusterQueueToken:\n\t\ttypename = \"ClusterQueueToken\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeClusterQueueToken\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeClusterToken:\n\t\ttypename = \"ClusterToken\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeClusterToken\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeEmail:\n\t\ttypename = \"Email\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeEmail\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeJobEventAssigned:\n\t\ttypename = \"JobEventAssigned\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeJobEventAssigned\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeJobEventBuildStepUploadCreated:\n\t\ttypename = \"JobEventBuildStepUploadCreated\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeJobEventBuildStepUploadCreated\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeJobEventCanceled:\n\t\ttypename = \"JobEventCanceled\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeJobEventCanceled\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeJobEventFinished:\n\t\ttypename = \"JobEventFinished\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeJobEventFinished\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeJobEventGeneric:\n\t\ttypename = \"JobEventGeneric\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeJobEventGeneric\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeJobEventRetried:\n\t\ttypename = \"JobEventRetried\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeJobEventRetried\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeJobEventTimedOut:\n\t\ttypename = \"JobEventTimedOut\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeJobEventTimedOut\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeJobTypeBlock:\n\t\ttypename = \"JobTypeBlock\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeJobTypeBlock\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeJobTypeCommand:\n\t\ttypename = \"JobTypeCommand\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeJobTypeCommand\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeJobTypeTrigger:\n\t\ttypename = \"JobTypeTrigger\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeJobTypeTrigger\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeJobTypeWait:\n\t\ttypename = \"JobTypeWait\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeJobTypeWait\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeNotificationServiceSlack:\n\t\ttypename = \"NotificationServiceSlack\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeNotificationServiceSlack\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeOrganization:\n\t\ttypename = \"Organization\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeOrganization\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeOrganizationBanner:\n\t\ttypename = \"OrganizationBanner\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeOrganizationBanner\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeOrganizationInvitation:\n\t\ttypename = \"OrganizationInvitation\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeOrganizationInvitation\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeOrganizationMember:\n\t\ttypename = \"OrganizationMember\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeOrganizationMember\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodePipeline:\n\t\ttypename = \"Pipeline\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodePipeline\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodePipelineMetric:\n\t\ttypename = \"PipelineMetric\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodePipelineMetric\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodePipelineSchedule:\n\t\ttypename = \"PipelineSchedule\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodePipelineSchedule\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodePipelineTemplate:\n\t\ttypename = \"PipelineTemplate\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodePipelineTemplate\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeSSOProviderGitHubApp:\n\t\ttypename = \"SSOProviderGitHubApp\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeSSOProviderGitHubApp\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeSSOProviderGoogleGSuite:\n\t\ttypename = \"SSOProviderGoogleGSuite\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeSSOProviderGoogleGSuite\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeSSOProviderSAML:\n\t\ttypename = \"SSOProviderSAML\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeSSOProviderSAML\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeSecret:\n\t\ttypename = \"Secret\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeSecret\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeSuite:\n\t\ttypename = \"Suite\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeSuite\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeTeam:\n\t\ttypename = \"Team\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeTeam\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeTeamMember:\n\t\ttypename = \"TeamMember\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeTeamMember\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeTeamPipeline:\n\t\ttypename = \"TeamPipeline\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeTeamPipeline\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeTeamSuite:\n\t\ttypename = \"TeamSuite\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeTeamSuite\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeUser:\n\t\ttypename = \"User\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeUser\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *FindQueuesForClusterNodeViewer:\n\t\ttypename = \"Viewer\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*FindQueuesForClusterNodeViewer\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase nil:\n\t\treturn []byte(\"null\"), nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\n\t\t\t`unexpected concrete type for FindQueuesForClusterNode: \"%T\"`, v)\n\t}\n}\n\n// FindQueuesForClusterNodeAPIAccessToken includes the requested fields of the GraphQL type APIAccessToken.\n// The GraphQL type's documentation follows.\n//\n// API access tokens for authentication with the Buildkite API\ntype FindQueuesForClusterNodeAPIAccessToken struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAPIAccessToken.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAPIAccessToken) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeAPIAccessTokenCode includes the requested fields of the GraphQL type APIAccessTokenCode.\n// The GraphQL type's documentation follows.\n//\n// A code that is used by an API Application to request an API Access Token\ntype FindQueuesForClusterNodeAPIAccessTokenCode struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAPIAccessTokenCode.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAPIAccessTokenCode) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeAPIApplication includes the requested fields of the GraphQL type APIApplication.\n// The GraphQL type's documentation follows.\n//\n// An API Application\ntype FindQueuesForClusterNodeAPIApplication struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAPIApplication.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAPIApplication) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeAgent includes the requested fields of the GraphQL type Agent.\n// The GraphQL type's documentation follows.\n//\n// An agent\ntype FindQueuesForClusterNodeAgent struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAgent.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAgent) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeAgentToken includes the requested fields of the GraphQL type AgentToken.\n// The GraphQL type's documentation follows.\n//\n// A token used to connect an agent to Buildkite\ntype FindQueuesForClusterNodeAgentToken struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAgentToken.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAgentToken) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeAnnotation includes the requested fields of the GraphQL type Annotation.\n// The GraphQL type's documentation follows.\n//\n// An annotation allows you to add arbitrary content to the top of a build page in the Buildkite UI\ntype FindQueuesForClusterNodeAnnotation struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAnnotation.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAnnotation) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeArtifact includes the requested fields of the GraphQL type Artifact.\n// The GraphQL type's documentation follows.\n//\n// A file uploaded from the agent whilst running a job\ntype FindQueuesForClusterNodeArtifact struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeArtifact.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeArtifact) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeAuditEvent includes the requested fields of the GraphQL type AuditEvent.\n// The GraphQL type's documentation follows.\n//\n// Audit record of an event which occurred in the system\ntype FindQueuesForClusterNodeAuditEvent struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAuditEvent.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAuditEvent) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeAuthorizationBitbucket includes the requested fields of the GraphQL type AuthorizationBitbucket.\n// The GraphQL type's documentation follows.\n//\n// A Bitbucket account authorized with a Buildkite account\ntype FindQueuesForClusterNodeAuthorizationBitbucket struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAuthorizationBitbucket.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAuthorizationBitbucket) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeAuthorizationGitHub includes the requested fields of the GraphQL type AuthorizationGitHub.\n// The GraphQL type's documentation follows.\n//\n// A GitHub account authorized with a Buildkite account\ntype FindQueuesForClusterNodeAuthorizationGitHub struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAuthorizationGitHub.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAuthorizationGitHub) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeAuthorizationGitHubApp includes the requested fields of the GraphQL type AuthorizationGitHubApp.\n// The GraphQL type's documentation follows.\n//\n// A GitHub app authorized with a Buildkite account\ntype FindQueuesForClusterNodeAuthorizationGitHubApp struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAuthorizationGitHubApp.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAuthorizationGitHubApp) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeAuthorizationGitHubEnterprise includes the requested fields of the GraphQL type AuthorizationGitHubEnterprise.\n// The GraphQL type's documentation follows.\n//\n// A GitHub Enterprise account authorized with a Buildkite account\ntype FindQueuesForClusterNodeAuthorizationGitHubEnterprise struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAuthorizationGitHubEnterprise.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAuthorizationGitHubEnterprise) GetTypename() *string {\n\treturn v.Typename\n}\n\n// FindQueuesForClusterNodeAuthorizationGoogle includes the requested fields of the GraphQL type AuthorizationGoogle.\n// The GraphQL type's documentation follows.\n//\n// A Google account authorized with a Buildkite account\ntype FindQueuesForClusterNodeAuthorizationGoogle struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAuthorizationGoogle.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAuthorizationGoogle) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeAuthorizationSAML includes the requested fields of the GraphQL type AuthorizationSAML.\n// The GraphQL type's documentation follows.\n//\n// A SAML account authorized with a Buildkite account\ntype FindQueuesForClusterNodeAuthorizationSAML struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeAuthorizationSAML.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeAuthorizationSAML) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeBuild includes the requested fields of the GraphQL type Build.\n// The GraphQL type's documentation follows.\n//\n// A build from a pipeline\ntype FindQueuesForClusterNodeBuild struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeBuild.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeBuild) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeChangelog includes the requested fields of the GraphQL type Changelog.\n// The GraphQL type's documentation follows.\n//\n// A changelog\ntype FindQueuesForClusterNodeChangelog struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeChangelog.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeChangelog) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeCluster includes the requested fields of the GraphQL type Cluster.\ntype FindQueuesForClusterNodeCluster struct {\n\tTypename *string `json:\"__typename\"`\n\tId       string  `json:\"id\"`\n\t// Name of the cluster\n\tName   string                                                       `json:\"name\"`\n\tQueues *FindQueuesForClusterNodeClusterQueuesClusterQueueConnection `json:\"queues\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeCluster.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeCluster) GetTypename() *string { return v.Typename }\n\n// GetId returns FindQueuesForClusterNodeCluster.Id, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeCluster) GetId() string { return v.Id }\n\n// GetName returns FindQueuesForClusterNodeCluster.Name, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeCluster) GetName() string { return v.Name }\n\n// GetQueues returns FindQueuesForClusterNodeCluster.Queues, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeCluster) GetQueues() *FindQueuesForClusterNodeClusterQueuesClusterQueueConnection {\n\treturn v.Queues\n}\n\n// FindQueuesForClusterNodeClusterQueue includes the requested fields of the GraphQL type ClusterQueue.\ntype FindQueuesForClusterNodeClusterQueue struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeClusterQueue.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeClusterQueue) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeClusterQueueToken includes the requested fields of the GraphQL type ClusterQueueToken.\n// The GraphQL type's documentation follows.\n//\n// A token used to register an agent with a Buildkite cluster queue\ntype FindQueuesForClusterNodeClusterQueueToken struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeClusterQueueToken.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeClusterQueueToken) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeClusterQueuesClusterQueueConnection includes the requested fields of the GraphQL type ClusterQueueConnection.\ntype FindQueuesForClusterNodeClusterQueuesClusterQueueConnection struct {\n\tEdges    []*FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge `json:\"edges\"`\n\tPageInfo *FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionPageInfo                `json:\"pageInfo\"`\n}\n\n// GetEdges returns FindQueuesForClusterNodeClusterQueuesClusterQueueConnection.Edges, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeClusterQueuesClusterQueueConnection) GetEdges() []*FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge {\n\treturn v.Edges\n}\n\n// GetPageInfo returns FindQueuesForClusterNodeClusterQueuesClusterQueueConnection.PageInfo, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeClusterQueuesClusterQueueConnection) GetPageInfo() *FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionPageInfo {\n\treturn v.PageInfo\n}\n\n// FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge includes the requested fields of the GraphQL type ClusterQueueEdge.\ntype FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge struct {\n\tNode *FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue `json:\"node\"`\n}\n\n// GetNode returns FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge.Node, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge) GetNode() *FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue {\n\treturn v.Node\n}\n\n// FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue includes the requested fields of the GraphQL type ClusterQueue.\ntype FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue struct {\n\tId  string `json:\"id\"`\n\tKey string `json:\"key\"`\n}\n\n// GetId returns FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue.Id, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue) GetId() string {\n\treturn v.Id\n}\n\n// GetKey returns FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue.Key, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue) GetKey() string {\n\treturn v.Key\n}\n\n// FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionPageInfo includes the requested fields of the GraphQL type PageInfo.\n// The GraphQL type's documentation follows.\n//\n// Information about pagination in a connection.\ntype FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionPageInfo struct {\n\t// When paginating forwards, are there more items?\n\tHasNextPage bool `json:\"hasNextPage\"`\n\t// When paginating forwards, the cursor to continue.\n\tEndCursor *string `json:\"endCursor\"`\n}\n\n// GetHasNextPage returns FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionPageInfo.HasNextPage, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionPageInfo) GetHasNextPage() bool {\n\treturn v.HasNextPage\n}\n\n// GetEndCursor returns FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionPageInfo.EndCursor, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeClusterQueuesClusterQueueConnectionPageInfo) GetEndCursor() *string {\n\treturn v.EndCursor\n}\n\n// FindQueuesForClusterNodeClusterToken includes the requested fields of the GraphQL type ClusterToken.\n// The GraphQL type's documentation follows.\n//\n// A token used to connect an agent in cluster to Buildkite\ntype FindQueuesForClusterNodeClusterToken struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeClusterToken.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeClusterToken) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeEmail includes the requested fields of the GraphQL type Email.\n// The GraphQL type's documentation follows.\n//\n// An email address\ntype FindQueuesForClusterNodeEmail struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeEmail.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeEmail) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeJobEventAssigned includes the requested fields of the GraphQL type JobEventAssigned.\n// The GraphQL type's documentation follows.\n//\n// An event created when the dispatcher assigns the job to an agent\ntype FindQueuesForClusterNodeJobEventAssigned struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeJobEventAssigned.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeJobEventAssigned) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeJobEventBuildStepUploadCreated includes the requested fields of the GraphQL type JobEventBuildStepUploadCreated.\n// The GraphQL type's documentation follows.\n//\n// An event created when the job creates new build steps via pipeline upload\ntype FindQueuesForClusterNodeJobEventBuildStepUploadCreated struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeJobEventBuildStepUploadCreated.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeJobEventBuildStepUploadCreated) GetTypename() *string {\n\treturn v.Typename\n}\n\n// FindQueuesForClusterNodeJobEventCanceled includes the requested fields of the GraphQL type JobEventCanceled.\n// The GraphQL type's documentation follows.\n//\n// An event created when the job is canceled\ntype FindQueuesForClusterNodeJobEventCanceled struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeJobEventCanceled.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeJobEventCanceled) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeJobEventFinished includes the requested fields of the GraphQL type JobEventFinished.\n// The GraphQL type's documentation follows.\n//\n// An event created when the job is finished\ntype FindQueuesForClusterNodeJobEventFinished struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeJobEventFinished.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeJobEventFinished) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeJobEventGeneric includes the requested fields of the GraphQL type JobEventGeneric.\n// The GraphQL type's documentation follows.\n//\n// A generic event type that doesn't have any additional meta-information associated with the event\ntype FindQueuesForClusterNodeJobEventGeneric struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeJobEventGeneric.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeJobEventGeneric) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeJobEventRetried includes the requested fields of the GraphQL type JobEventRetried.\n// The GraphQL type's documentation follows.\n//\n// An event created when the job is retried\ntype FindQueuesForClusterNodeJobEventRetried struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeJobEventRetried.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeJobEventRetried) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeJobEventTimedOut includes the requested fields of the GraphQL type JobEventTimedOut.\n// The GraphQL type's documentation follows.\n//\n// An event created when the job is timed out\ntype FindQueuesForClusterNodeJobEventTimedOut struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeJobEventTimedOut.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeJobEventTimedOut) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeJobTypeBlock includes the requested fields of the GraphQL type JobTypeBlock.\n// The GraphQL type's documentation follows.\n//\n// A type of job that requires a user to unblock it before proceeding in a build pipeline\ntype FindQueuesForClusterNodeJobTypeBlock struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeJobTypeBlock.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeJobTypeBlock) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeJobTypeCommand includes the requested fields of the GraphQL type JobTypeCommand.\n// The GraphQL type's documentation follows.\n//\n// A type of job that runs a command on an agent\ntype FindQueuesForClusterNodeJobTypeCommand struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeJobTypeCommand.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeJobTypeCommand) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeJobTypeTrigger includes the requested fields of the GraphQL type JobTypeTrigger.\n// The GraphQL type's documentation follows.\n//\n// A type of job that triggers another build on a pipeline\ntype FindQueuesForClusterNodeJobTypeTrigger struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeJobTypeTrigger.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeJobTypeTrigger) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeJobTypeWait includes the requested fields of the GraphQL type JobTypeWait.\n// The GraphQL type's documentation follows.\n//\n// A type of job that waits for all previous jobs to pass before proceeding the build pipeline\ntype FindQueuesForClusterNodeJobTypeWait struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeJobTypeWait.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeJobTypeWait) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeNotificationServiceSlack includes the requested fields of the GraphQL type NotificationServiceSlack.\n// The GraphQL type's documentation follows.\n//\n// Deliver notifications to Slack\ntype FindQueuesForClusterNodeNotificationServiceSlack struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeNotificationServiceSlack.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeNotificationServiceSlack) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeOrganization includes the requested fields of the GraphQL type Organization.\n// The GraphQL type's documentation follows.\n//\n// An organization\ntype FindQueuesForClusterNodeOrganization struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeOrganization.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeOrganization) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeOrganizationBanner includes the requested fields of the GraphQL type OrganizationBanner.\n// The GraphQL type's documentation follows.\n//\n// System banner of an organization\ntype FindQueuesForClusterNodeOrganizationBanner struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeOrganizationBanner.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeOrganizationBanner) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeOrganizationInvitation includes the requested fields of the GraphQL type OrganizationInvitation.\n// The GraphQL type's documentation follows.\n//\n// A pending invitation to a user to join this organization\ntype FindQueuesForClusterNodeOrganizationInvitation struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeOrganizationInvitation.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeOrganizationInvitation) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeOrganizationMember includes the requested fields of the GraphQL type OrganizationMember.\n// The GraphQL type's documentation follows.\n//\n// A member of an organization\ntype FindQueuesForClusterNodeOrganizationMember struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeOrganizationMember.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeOrganizationMember) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodePipeline includes the requested fields of the GraphQL type Pipeline.\n// The GraphQL type's documentation follows.\n//\n// A pipeline\ntype FindQueuesForClusterNodePipeline struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodePipeline.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodePipeline) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodePipelineMetric includes the requested fields of the GraphQL type PipelineMetric.\n// The GraphQL type's documentation follows.\n//\n// A metric for a pipeline\ntype FindQueuesForClusterNodePipelineMetric struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodePipelineMetric.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodePipelineMetric) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodePipelineSchedule includes the requested fields of the GraphQL type PipelineSchedule.\n// The GraphQL type's documentation follows.\n//\n// A schedule of when a build should automatically triggered for a Pipeline\ntype FindQueuesForClusterNodePipelineSchedule struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodePipelineSchedule.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodePipelineSchedule) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodePipelineTemplate includes the requested fields of the GraphQL type PipelineTemplate.\n// The GraphQL type's documentation follows.\n//\n// A template defining a fixed step configuration for a pipeline\ntype FindQueuesForClusterNodePipelineTemplate struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodePipelineTemplate.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodePipelineTemplate) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeSSOProviderGitHubApp includes the requested fields of the GraphQL type SSOProviderGitHubApp.\n// The GraphQL type's documentation follows.\n//\n// Single sign-on provided by GitHub\ntype FindQueuesForClusterNodeSSOProviderGitHubApp struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeSSOProviderGitHubApp.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeSSOProviderGitHubApp) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeSSOProviderGoogleGSuite includes the requested fields of the GraphQL type SSOProviderGoogleGSuite.\n// The GraphQL type's documentation follows.\n//\n// Single sign-on provided by Google\ntype FindQueuesForClusterNodeSSOProviderGoogleGSuite struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeSSOProviderGoogleGSuite.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeSSOProviderGoogleGSuite) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeSSOProviderSAML includes the requested fields of the GraphQL type SSOProviderSAML.\n// The GraphQL type's documentation follows.\n//\n// Single sign-on provided via SAML\ntype FindQueuesForClusterNodeSSOProviderSAML struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeSSOProviderSAML.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeSSOProviderSAML) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeSecret includes the requested fields of the GraphQL type Secret.\n// The GraphQL type's documentation follows.\n//\n// A secret hosted by Buildkite. This does not contain the secret value or encrypted material.\ntype FindQueuesForClusterNodeSecret struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeSecret.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeSecret) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeSuite includes the requested fields of the GraphQL type Suite.\n// The GraphQL type's documentation follows.\n//\n// A suite\ntype FindQueuesForClusterNodeSuite struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeSuite.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeSuite) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeTeam includes the requested fields of the GraphQL type Team.\n// The GraphQL type's documentation follows.\n//\n// An organization team\ntype FindQueuesForClusterNodeTeam struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeTeam.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeTeam) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeTeamMember includes the requested fields of the GraphQL type TeamMember.\n// The GraphQL type's documentation follows.\n//\n// An member of a team\ntype FindQueuesForClusterNodeTeamMember struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeTeamMember.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeTeamMember) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeTeamPipeline includes the requested fields of the GraphQL type TeamPipeline.\n// The GraphQL type's documentation follows.\n//\n// An pipeline that's been assigned to a team\ntype FindQueuesForClusterNodeTeamPipeline struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeTeamPipeline.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeTeamPipeline) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeTeamSuite includes the requested fields of the GraphQL type TeamSuite.\n// The GraphQL type's documentation follows.\n//\n// A suite that's been assigned to a team\ntype FindQueuesForClusterNodeTeamSuite struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeTeamSuite.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeTeamSuite) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeUser includes the requested fields of the GraphQL type User.\n// The GraphQL type's documentation follows.\n//\n// A user\ntype FindQueuesForClusterNodeUser struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeUser.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeUser) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterNodeViewer includes the requested fields of the GraphQL type Viewer.\n// The GraphQL type's documentation follows.\n//\n// Represents the current user session\ntype FindQueuesForClusterNodeViewer struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns FindQueuesForClusterNodeViewer.Typename, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterNodeViewer) GetTypename() *string { return v.Typename }\n\n// FindQueuesForClusterResponse is returned by FindQueuesForCluster on success.\ntype FindQueuesForClusterResponse struct {\n\t// Fetches an object given its ID.\n\tNode *FindQueuesForClusterNode `json:\"-\"`\n}\n\n// GetNode returns FindQueuesForClusterResponse.Node, and is useful for accessing the field via an interface.\nfunc (v *FindQueuesForClusterResponse) GetNode() *FindQueuesForClusterNode { return v.Node }\n\nfunc (v *FindQueuesForClusterResponse) UnmarshalJSON(b []byte) error {\n\n\tif string(b) == \"null\" {\n\t\treturn nil\n\t}\n\n\tvar firstPass struct {\n\t\t*FindQueuesForClusterResponse\n\t\tNode json.RawMessage `json:\"node\"`\n\t\tgraphql.NoUnmarshalJSON\n\t}\n\tfirstPass.FindQueuesForClusterResponse = v\n\n\terr := json.Unmarshal(b, &firstPass)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t{\n\t\tdst := &v.Node\n\t\tsrc := firstPass.Node\n\t\tif len(src) != 0 && string(src) != \"null\" {\n\t\t\t*dst = new(FindQueuesForClusterNode)\n\t\t\terr = __unmarshalFindQueuesForClusterNode(\n\t\t\t\tsrc, *dst)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\"unable to unmarshal FindQueuesForClusterResponse.Node: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\ntype __premarshalFindQueuesForClusterResponse struct {\n\tNode json.RawMessage `json:\"node\"`\n}\n\nfunc (v *FindQueuesForClusterResponse) MarshalJSON() ([]byte, error) {\n\tpremarshaled, err := v.__premarshalJSON()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn json.Marshal(premarshaled)\n}\n\nfunc (v *FindQueuesForClusterResponse) __premarshalJSON() (*__premarshalFindQueuesForClusterResponse, error) {\n\tvar retval __premarshalFindQueuesForClusterResponse\n\n\t{\n\n\t\tdst := &retval.Node\n\t\tsrc := v.Node\n\t\tif src != nil {\n\t\t\tvar err error\n\t\t\t*dst, err = __marshalFindQueuesForClusterNode(\n\t\t\t\tsrc)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\t\"unable to marshal FindQueuesForClusterResponse.Node: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn &retval, nil\n}\n\n// FindUserByEmailOrganization includes the requested fields of the GraphQL type Organization.\n// The GraphQL type's documentation follows.\n//\n// An organization\ntype FindUserByEmailOrganization struct {\n\t// Returns users within the organization\n\tMembers *FindUserByEmailOrganizationMembersOrganizationMemberConnection `json:\"members\"`\n}\n\n// GetMembers returns FindUserByEmailOrganization.Members, and is useful for accessing the field via an interface.\nfunc (v *FindUserByEmailOrganization) GetMembers() *FindUserByEmailOrganizationMembersOrganizationMemberConnection {\n\treturn v.Members\n}\n\n// FindUserByEmailOrganizationMembersOrganizationMemberConnection includes the requested fields of the GraphQL type OrganizationMemberConnection.\ntype FindUserByEmailOrganizationMembersOrganizationMemberConnection struct {\n\tEdges []*FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdge `json:\"edges\"`\n}\n\n// GetEdges returns FindUserByEmailOrganizationMembersOrganizationMemberConnection.Edges, and is useful for accessing the field via an interface.\nfunc (v *FindUserByEmailOrganizationMembersOrganizationMemberConnection) GetEdges() []*FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdge {\n\treturn v.Edges\n}\n\n// FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdge includes the requested fields of the GraphQL type OrganizationMemberEdge.\ntype FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdge struct {\n\tNode *FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMember `json:\"node\"`\n}\n\n// GetNode returns FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdge.Node, and is useful for accessing the field via an interface.\nfunc (v *FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdge) GetNode() *FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMember {\n\treturn v.Node\n}\n\n// FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMember includes the requested fields of the GraphQL type OrganizationMember.\n// The GraphQL type's documentation follows.\n//\n// A member of an organization\ntype FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMember struct {\n\tUser FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMemberUser `json:\"user\"`\n}\n\n// GetUser returns FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMember.User, and is useful for accessing the field via an interface.\nfunc (v *FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMember) GetUser() FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMemberUser {\n\treturn v.User\n}\n\n// FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMemberUser includes the requested fields of the GraphQL type User.\n// The GraphQL type's documentation follows.\n//\n// A user\ntype FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMemberUser struct {\n\tId string `json:\"id\"`\n}\n\n// GetId returns FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMemberUser.Id, and is useful for accessing the field via an interface.\nfunc (v *FindUserByEmailOrganizationMembersOrganizationMemberConnectionEdgesOrganizationMemberEdgeNodeOrganizationMemberUser) GetId() string {\n\treturn v.Id\n}\n\n// FindUserByEmailResponse is returned by FindUserByEmail on success.\ntype FindUserByEmailResponse struct {\n\t// Find an organization\n\tOrganization *FindUserByEmailOrganization `json:\"organization\"`\n}\n\n// GetOrganization returns FindUserByEmailResponse.Organization, and is useful for accessing the field via an interface.\nfunc (v *FindUserByEmailResponse) GetOrganization() *FindUserByEmailOrganization {\n\treturn v.Organization\n}\n\n// GetClusterQueueAgentOrganization includes the requested fields of the GraphQL type Organization.\n// The GraphQL type's documentation follows.\n//\n// An organization\ntype GetClusterQueueAgentOrganization struct {\n\tAgents *GetClusterQueueAgentOrganizationAgentsAgentConnection `json:\"agents\"`\n}\n\n// GetAgents returns GetClusterQueueAgentOrganization.Agents, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueueAgentOrganization) GetAgents() *GetClusterQueueAgentOrganizationAgentsAgentConnection {\n\treturn v.Agents\n}\n\n// GetClusterQueueAgentOrganizationAgentsAgentConnection includes the requested fields of the GraphQL type AgentConnection.\ntype GetClusterQueueAgentOrganizationAgentsAgentConnection struct {\n\tEdges []*GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdge `json:\"edges\"`\n}\n\n// GetEdges returns GetClusterQueueAgentOrganizationAgentsAgentConnection.Edges, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueueAgentOrganizationAgentsAgentConnection) GetEdges() []*GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdge {\n\treturn v.Edges\n}\n\n// GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdge includes the requested fields of the GraphQL type AgentEdge.\ntype GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdge struct {\n\tNode *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent `json:\"node\"`\n}\n\n// GetNode returns GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdge.Node, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdge) GetNode() *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent {\n\treturn v.Node\n}\n\n// GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent includes the requested fields of the GraphQL type Agent.\n// The GraphQL type's documentation follows.\n//\n// An agent\ntype GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent struct {\n\t// The name of the agent\n\tName string `json:\"name\"`\n\t// The hostname of the machine running the agent\n\tHostname *string `json:\"hostname\"`\n\t// The version of the agent\n\tVersion      *string                                                                                   `json:\"version\"`\n\tId           string                                                                                    `json:\"id\"`\n\tClusterQueue *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgentClusterQueue `json:\"clusterQueue\"`\n}\n\n// GetName returns GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent.Name, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent) GetName() string {\n\treturn v.Name\n}\n\n// GetHostname returns GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent.Hostname, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent) GetHostname() *string {\n\treturn v.Hostname\n}\n\n// GetVersion returns GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent.Version, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent) GetVersion() *string {\n\treturn v.Version\n}\n\n// GetId returns GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent.Id, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent) GetId() string {\n\treturn v.Id\n}\n\n// GetClusterQueue returns GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent.ClusterQueue, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgent) GetClusterQueue() *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgentClusterQueue {\n\treturn v.ClusterQueue\n}\n\n// GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgentClusterQueue includes the requested fields of the GraphQL type ClusterQueue.\ntype GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgentClusterQueue struct {\n\tId string `json:\"id\"`\n\t// The public UUID for this cluster queue\n\tUuid string `json:\"uuid\"`\n}\n\n// GetId returns GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgentClusterQueue.Id, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgentClusterQueue) GetId() string {\n\treturn v.Id\n}\n\n// GetUuid returns GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgentClusterQueue.Uuid, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueueAgentOrganizationAgentsAgentConnectionEdgesAgentEdgeNodeAgentClusterQueue) GetUuid() string {\n\treturn v.Uuid\n}\n\n// GetClusterQueueAgentResponse is returned by GetClusterQueueAgent on success.\ntype GetClusterQueueAgentResponse struct {\n\t// Find an organization\n\tOrganization *GetClusterQueueAgentOrganization `json:\"organization\"`\n}\n\n// GetOrganization returns GetClusterQueueAgentResponse.Organization, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueueAgentResponse) GetOrganization() *GetClusterQueueAgentOrganization {\n\treturn v.Organization\n}\n\n// GetClusterQueuesOrganization includes the requested fields of the GraphQL type Organization.\n// The GraphQL type's documentation follows.\n//\n// An organization\ntype GetClusterQueuesOrganization struct {\n\t// Return cluster in the Organization by UUID\n\tCluster *GetClusterQueuesOrganizationCluster `json:\"cluster\"`\n}\n\n// GetCluster returns GetClusterQueuesOrganization.Cluster, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueuesOrganization) GetCluster() *GetClusterQueuesOrganizationCluster {\n\treturn v.Cluster\n}\n\n// GetClusterQueuesOrganizationCluster includes the requested fields of the GraphQL type Cluster.\ntype GetClusterQueuesOrganizationCluster struct {\n\t// Name of the cluster\n\tName string `json:\"name\"`\n\t// Description of the cluster\n\tDescription *string                                                          `json:\"description\"`\n\tQueues      *GetClusterQueuesOrganizationClusterQueuesClusterQueueConnection `json:\"queues\"`\n}\n\n// GetName returns GetClusterQueuesOrganizationCluster.Name, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueuesOrganizationCluster) GetName() string { return v.Name }\n\n// GetDescription returns GetClusterQueuesOrganizationCluster.Description, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueuesOrganizationCluster) GetDescription() *string { return v.Description }\n\n// GetQueues returns GetClusterQueuesOrganizationCluster.Queues, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueuesOrganizationCluster) GetQueues() *GetClusterQueuesOrganizationClusterQueuesClusterQueueConnection {\n\treturn v.Queues\n}\n\n// GetClusterQueuesOrganizationClusterQueuesClusterQueueConnection includes the requested fields of the GraphQL type ClusterQueueConnection.\ntype GetClusterQueuesOrganizationClusterQueuesClusterQueueConnection struct {\n\tEdges []*GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge `json:\"edges\"`\n}\n\n// GetEdges returns GetClusterQueuesOrganizationClusterQueuesClusterQueueConnection.Edges, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueuesOrganizationClusterQueuesClusterQueueConnection) GetEdges() []*GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge {\n\treturn v.Edges\n}\n\n// GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge includes the requested fields of the GraphQL type ClusterQueueEdge.\ntype GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge struct {\n\tNode *GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue `json:\"node\"`\n}\n\n// GetNode returns GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge.Node, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdge) GetNode() *GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue {\n\treturn v.Node\n}\n\n// GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue includes the requested fields of the GraphQL type ClusterQueue.\ntype GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue struct {\n\tId string `json:\"id\"`\n\t// The public UUID for this cluster queue\n\tUuid        string  `json:\"uuid\"`\n\tKey         string  `json:\"key\"`\n\tDescription *string `json:\"description\"`\n}\n\n// GetId returns GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue.Id, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue) GetId() string {\n\treturn v.Id\n}\n\n// GetUuid returns GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue.Uuid, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue) GetUuid() string {\n\treturn v.Uuid\n}\n\n// GetKey returns GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue.Key, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue) GetKey() string {\n\treturn v.Key\n}\n\n// GetDescription returns GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue.Description, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueuesOrganizationClusterQueuesClusterQueueConnectionEdgesClusterQueueEdgeNodeClusterQueue) GetDescription() *string {\n\treturn v.Description\n}\n\n// GetClusterQueuesResponse is returned by GetClusterQueues on success.\ntype GetClusterQueuesResponse struct {\n\t// Find an organization\n\tOrganization *GetClusterQueuesOrganization `json:\"organization\"`\n}\n\n// GetOrganization returns GetClusterQueuesResponse.Organization, and is useful for accessing the field via an interface.\nfunc (v *GetClusterQueuesResponse) GetOrganization() *GetClusterQueuesOrganization {\n\treturn v.Organization\n}\n\n// GetOrganizationIDOrganization includes the requested fields of the GraphQL type Organization.\n// The GraphQL type's documentation follows.\n//\n// An organization\ntype GetOrganizationIDOrganization struct {\n\tId string `json:\"id\"`\n}\n\n// GetId returns GetOrganizationIDOrganization.Id, and is useful for accessing the field via an interface.\nfunc (v *GetOrganizationIDOrganization) GetId() string { return v.Id }\n\n// GetOrganizationIDResponse is returned by GetOrganizationID on success.\ntype GetOrganizationIDResponse struct {\n\t// Find an organization\n\tOrganization *GetOrganizationIDOrganization `json:\"organization\"`\n}\n\n// GetOrganization returns GetOrganizationIDResponse.Organization, and is useful for accessing the field via an interface.\nfunc (v *GetOrganizationIDResponse) GetOrganization() *GetOrganizationIDOrganization {\n\treturn v.Organization\n}\n\n// InviteUserOrganizationInvitationCreateOrganizationInvitationCreatePayload includes the requested fields of the GraphQL type OrganizationInvitationCreatePayload.\n// The GraphQL type's documentation follows.\n//\n// Autogenerated return type of OrganizationInvitationCreate.\ntype InviteUserOrganizationInvitationCreateOrganizationInvitationCreatePayload struct {\n\t// A unique identifier for the client performing the mutation.\n\tClientMutationId *string `json:\"clientMutationId\"`\n}\n\n// GetClientMutationId returns InviteUserOrganizationInvitationCreateOrganizationInvitationCreatePayload.ClientMutationId, and is useful for accessing the field via an interface.\nfunc (v *InviteUserOrganizationInvitationCreateOrganizationInvitationCreatePayload) GetClientMutationId() *string {\n\treturn v.ClientMutationId\n}\n\n// InviteUserResponse is returned by InviteUser on success.\ntype InviteUserResponse struct {\n\t// Send email invitations to this organization.\n\tOrganizationInvitationCreate *InviteUserOrganizationInvitationCreateOrganizationInvitationCreatePayload `json:\"organizationInvitationCreate\"`\n}\n\n// GetOrganizationInvitationCreate returns InviteUserResponse.OrganizationInvitationCreate, and is useful for accessing the field via an interface.\nfunc (v *InviteUserResponse) GetOrganizationInvitationCreate() *InviteUserOrganizationInvitationCreateOrganizationInvitationCreatePayload {\n\treturn v.OrganizationInvitationCreate\n}\n\n// All the possible states a job can be in\ntype JobStates string\n\nconst (\n\t// The job has just been created and doesn't have a state yet\n\tJobStatesPending JobStates = \"PENDING\"\n\t// The job is waiting on a `wait` step to finish\n\tJobStatesWaiting JobStates = \"WAITING\"\n\t// The job was in a `WAITING` state when the build failed\n\tJobStatesWaitingFailed JobStates = \"WAITING_FAILED\"\n\t// The job is waiting on a `block` step to finish\n\tJobStatesBlocked JobStates = \"BLOCKED\"\n\t// The job was in a `BLOCKED` state when the build failed\n\tJobStatesBlockedFailed JobStates = \"BLOCKED_FAILED\"\n\t// This `block` job has been manually unblocked\n\tJobStatesUnblocked JobStates = \"UNBLOCKED\"\n\t// This `block` job was in an `UNBLOCKED` state when the build failed\n\tJobStatesUnblockedFailed JobStates = \"UNBLOCKED_FAILED\"\n\t// The job is waiting on a concurrency group check before becoming either `LIMITED` or `SCHEDULED`\n\tJobStatesLimiting JobStates = \"LIMITING\"\n\t// The job is waiting for jobs with the same concurrency group to finish\n\tJobStatesLimited JobStates = \"LIMITED\"\n\t// The job is scheduled and waiting for an agent\n\tJobStatesScheduled JobStates = \"SCHEDULED\"\n\t// The job has been assigned to an agent, and it's waiting for it to accept\n\tJobStatesAssigned JobStates = \"ASSIGNED\"\n\t// The job was accepted by the agent, and now it's waiting to start running\n\tJobStatesAccepted JobStates = \"ACCEPTED\"\n\t// The job is running\n\tJobStatesRunning JobStates = \"RUNNING\"\n\t// The job has finished\n\tJobStatesFinished JobStates = \"FINISHED\"\n\t// The job is currently canceling\n\tJobStatesCanceling JobStates = \"CANCELING\"\n\t// The job was canceled\n\tJobStatesCanceled JobStates = \"CANCELED\"\n\t// The job is timing out for taking too long\n\tJobStatesTimingOut JobStates = \"TIMING_OUT\"\n\t// The job timed out\n\tJobStatesTimedOut JobStates = \"TIMED_OUT\"\n\t// The job was skipped\n\tJobStatesSkipped JobStates = \"SKIPPED\"\n\t// The jobs configuration means that it can't be run\n\tJobStatesBroken JobStates = \"BROKEN\"\n\t// The job expired before it was started on an agent\n\tJobStatesExpired JobStates = \"EXPIRED\"\n)\n\nvar AllJobStates = []JobStates{\n\tJobStatesPending,\n\tJobStatesWaiting,\n\tJobStatesWaitingFailed,\n\tJobStatesBlocked,\n\tJobStatesBlockedFailed,\n\tJobStatesUnblocked,\n\tJobStatesUnblockedFailed,\n\tJobStatesLimiting,\n\tJobStatesLimited,\n\tJobStatesScheduled,\n\tJobStatesAssigned,\n\tJobStatesAccepted,\n\tJobStatesRunning,\n\tJobStatesFinished,\n\tJobStatesCanceling,\n\tJobStatesCanceled,\n\tJobStatesTimingOut,\n\tJobStatesTimedOut,\n\tJobStatesSkipped,\n\tJobStatesBroken,\n\tJobStatesExpired,\n}\n\n// ListJobsByAgentQueryRulesOrganization includes the requested fields of the GraphQL type Organization.\n// The GraphQL type's documentation follows.\n//\n// An organization\ntype ListJobsByAgentQueryRulesOrganization struct {\n\tJobs *ListJobsByAgentQueryRulesOrganizationJobsJobConnection `json:\"jobs\"`\n}\n\n// GetJobs returns ListJobsByAgentQueryRulesOrganization.Jobs, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganization) GetJobs() *ListJobsByAgentQueryRulesOrganizationJobsJobConnection {\n\treturn v.Jobs\n}\n\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnection includes the requested fields of the GraphQL type JobConnection.\ntype ListJobsByAgentQueryRulesOrganizationJobsJobConnection struct {\n\tEdges    []*ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge `json:\"edges\"`\n\tPageInfo *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionPageInfo       `json:\"pageInfo\"`\n}\n\n// GetEdges returns ListJobsByAgentQueryRulesOrganizationJobsJobConnection.Edges, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnection) GetEdges() []*ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge {\n\treturn v.Edges\n}\n\n// GetPageInfo returns ListJobsByAgentQueryRulesOrganizationJobsJobConnection.PageInfo, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnection) GetPageInfo() *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionPageInfo {\n\treturn v.PageInfo\n}\n\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge includes the requested fields of the GraphQL type JobEdge.\ntype ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge struct {\n\tNode *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob `json:\"-\"`\n}\n\n// GetNode returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge.Node, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge) GetNode() *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob {\n\treturn v.Node\n}\n\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge) UnmarshalJSON(b []byte) error {\n\n\tif string(b) == \"null\" {\n\t\treturn nil\n\t}\n\n\tvar firstPass struct {\n\t\t*ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge\n\t\tNode json.RawMessage `json:\"node\"`\n\t\tgraphql.NoUnmarshalJSON\n\t}\n\tfirstPass.ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge = v\n\n\terr := json.Unmarshal(b, &firstPass)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t{\n\t\tdst := &v.Node\n\t\tsrc := firstPass.Node\n\t\tif len(src) != 0 && string(src) != \"null\" {\n\t\t\t*dst = new(ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob)\n\t\t\terr = __unmarshalListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(\n\t\t\t\tsrc, *dst)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\"unable to unmarshal ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge.Node: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\ntype __premarshalListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge struct {\n\tNode json.RawMessage `json:\"node\"`\n}\n\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge) MarshalJSON() ([]byte, error) {\n\tpremarshaled, err := v.__premarshalJSON()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn json.Marshal(premarshaled)\n}\n\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge) __premarshalJSON() (*__premarshalListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge, error) {\n\tvar retval __premarshalListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge\n\n\t{\n\n\t\tdst := &retval.Node\n\t\tsrc := v.Node\n\t\tif src != nil {\n\t\t\tvar err error\n\t\t\t*dst, err = __marshalListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(\n\t\t\t\tsrc)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\t\"unable to marshal ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdge.Node: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn &retval, nil\n}\n\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob includes the requested fields of the GraphQL interface Job.\n//\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob is implemented by the following types:\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait\n// The GraphQL type's documentation follows.\n//\n// Kinds of jobs that can exist on a build\ntype ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob interface {\n\timplementsGraphQLInterfaceListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob()\n\t// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).\n\tGetTypename() *string\n}\n\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock) implementsGraphQLInterfaceListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) implementsGraphQLInterfaceListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger) implementsGraphQLInterfaceListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait) implementsGraphQLInterfaceListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\n\nfunc __unmarshalListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(b []byte, v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob) error {\n\tif string(b) == \"null\" {\n\t\treturn nil\n\t}\n\n\tvar tn struct {\n\t\tTypeName string `json:\"__typename\"`\n\t}\n\terr := json.Unmarshal(b, &tn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch tn.TypeName {\n\tcase \"JobTypeBlock\":\n\t\t*v = new(ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeCommand\":\n\t\t*v = new(ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeTrigger\":\n\t\t*v = new(ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeWait\":\n\t\t*v = new(ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"\":\n\t\treturn fmt.Errorf(\n\t\t\t\"response was missing Job.__typename\")\n\tdefault:\n\t\treturn fmt.Errorf(\n\t\t\t`unexpected concrete type for ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob: \"%v\"`, tn.TypeName)\n\t}\n}\n\nfunc __marshalListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob) ([]byte, error) {\n\n\tvar typename string\n\tswitch v := (*v).(type) {\n\tcase *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock:\n\t\ttypename = \"JobTypeBlock\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand:\n\t\ttypename = \"JobTypeCommand\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger:\n\t\ttypename = \"JobTypeTrigger\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait:\n\t\ttypename = \"JobTypeWait\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase nil:\n\t\treturn []byte(\"null\"), nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\n\t\t\t`unexpected concrete type for ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJob: \"%T\"`, v)\n\t}\n}\n\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock includes the requested fields of the GraphQL type JobTypeBlock.\n// The GraphQL type's documentation follows.\n//\n// A type of job that requires a user to unblock it before proceeding in a build pipeline\ntype ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock) GetTypename() *string {\n\treturn v.Typename\n}\n\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand includes the requested fields of the GraphQL type JobTypeCommand.\n// The GraphQL type's documentation follows.\n//\n// A type of job that runs a command on an agent\ntype ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand struct {\n\tTypename *string `json:\"__typename\"`\n\tId       string  `json:\"id\"`\n\t// The UUID for this job\n\tUuid string `json:\"uuid\"`\n\t// The command the job will run\n\tCommand *string `json:\"command\"`\n\t// The state of the job\n\tState JobStates `json:\"state\"`\n\t// The exit status returned by the command on the agent\n\tExitStatus *string `json:\"exitStatus\"`\n\t// The URL for the job\n\tUrl string `json:\"url\"`\n\t// The time when the job started running\n\tStartedAt *time.Time `json:\"startedAt\"`\n\t// The time when the job finished\n\tFinishedAt *time.Time `json:\"finishedAt\"`\n\t// The time when the job was created\n\tCreatedAt *time.Time `json:\"createdAt\"`\n\t// The agent that is running the job\n\tAgent *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent `json:\"agent\"`\n}\n\n// GetTypename returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetTypename() *string {\n\treturn v.Typename\n}\n\n// GetId returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Id, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetId() string {\n\treturn v.Id\n}\n\n// GetUuid returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Uuid, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetUuid() string {\n\treturn v.Uuid\n}\n\n// GetCommand returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Command, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetCommand() *string {\n\treturn v.Command\n}\n\n// GetState returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.State, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetState() JobStates {\n\treturn v.State\n}\n\n// GetExitStatus returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.ExitStatus, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetExitStatus() *string {\n\treturn v.ExitStatus\n}\n\n// GetUrl returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Url, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetUrl() string {\n\treturn v.Url\n}\n\n// GetStartedAt returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.StartedAt, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetStartedAt() *time.Time {\n\treturn v.StartedAt\n}\n\n// GetFinishedAt returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.FinishedAt, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetFinishedAt() *time.Time {\n\treturn v.FinishedAt\n}\n\n// GetCreatedAt returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.CreatedAt, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetCreatedAt() *time.Time {\n\treturn v.CreatedAt\n}\n\n// GetAgent returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Agent, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetAgent() *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent {\n\treturn v.Agent\n}\n\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent includes the requested fields of the GraphQL type Agent.\n// The GraphQL type's documentation follows.\n//\n// An agent\ntype ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent struct {\n\tId string `json:\"id\"`\n\t// The name of the agent\n\tName string `json:\"name\"`\n\t// The hostname of the machine running the agent\n\tHostname *string `json:\"hostname\"`\n\t// The meta data this agent was stared with\n\tMetaData []string `json:\"metaData\"`\n}\n\n// GetId returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.Id, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetId() string {\n\treturn v.Id\n}\n\n// GetName returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.Name, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetName() string {\n\treturn v.Name\n}\n\n// GetHostname returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.Hostname, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetHostname() *string {\n\treturn v.Hostname\n}\n\n// GetMetaData returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.MetaData, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetMetaData() []string {\n\treturn v.MetaData\n}\n\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger includes the requested fields of the GraphQL type JobTypeTrigger.\n// The GraphQL type's documentation follows.\n//\n// A type of job that triggers another build on a pipeline\ntype ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger) GetTypename() *string {\n\treturn v.Typename\n}\n\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait includes the requested fields of the GraphQL type JobTypeWait.\n// The GraphQL type's documentation follows.\n//\n// A type of job that waits for all previous jobs to pass before proceeding the build pipeline\ntype ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait) GetTypename() *string {\n\treturn v.Typename\n}\n\n// ListJobsByAgentQueryRulesOrganizationJobsJobConnectionPageInfo includes the requested fields of the GraphQL type PageInfo.\n// The GraphQL type's documentation follows.\n//\n// Information about pagination in a connection.\ntype ListJobsByAgentQueryRulesOrganizationJobsJobConnectionPageInfo struct {\n\t// When paginating forwards, the cursor to continue.\n\tEndCursor *string `json:\"endCursor\"`\n\t// When paginating forwards, are there more items?\n\tHasNextPage bool `json:\"hasNextPage\"`\n}\n\n// GetEndCursor returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionPageInfo.EndCursor, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionPageInfo) GetEndCursor() *string {\n\treturn v.EndCursor\n}\n\n// GetHasNextPage returns ListJobsByAgentQueryRulesOrganizationJobsJobConnectionPageInfo.HasNextPage, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesOrganizationJobsJobConnectionPageInfo) GetHasNextPage() bool {\n\treturn v.HasNextPage\n}\n\n// ListJobsByAgentQueryRulesResponse is returned by ListJobsByAgentQueryRules on success.\ntype ListJobsByAgentQueryRulesResponse struct {\n\t// Find an organization\n\tOrganization *ListJobsByAgentQueryRulesOrganization `json:\"organization\"`\n}\n\n// GetOrganization returns ListJobsByAgentQueryRulesResponse.Organization, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByAgentQueryRulesResponse) GetOrganization() *ListJobsByAgentQueryRulesOrganization {\n\treturn v.Organization\n}\n\n// ListJobsByQueueOrganization includes the requested fields of the GraphQL type Organization.\n// The GraphQL type's documentation follows.\n//\n// An organization\ntype ListJobsByQueueOrganization struct {\n\tJobs *ListJobsByQueueOrganizationJobsJobConnection `json:\"jobs\"`\n}\n\n// GetJobs returns ListJobsByQueueOrganization.Jobs, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganization) GetJobs() *ListJobsByQueueOrganizationJobsJobConnection {\n\treturn v.Jobs\n}\n\n// ListJobsByQueueOrganizationJobsJobConnection includes the requested fields of the GraphQL type JobConnection.\ntype ListJobsByQueueOrganizationJobsJobConnection struct {\n\tEdges    []*ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge `json:\"edges\"`\n\tPageInfo *ListJobsByQueueOrganizationJobsJobConnectionPageInfo       `json:\"pageInfo\"`\n}\n\n// GetEdges returns ListJobsByQueueOrganizationJobsJobConnection.Edges, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnection) GetEdges() []*ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge {\n\treturn v.Edges\n}\n\n// GetPageInfo returns ListJobsByQueueOrganizationJobsJobConnection.PageInfo, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnection) GetPageInfo() *ListJobsByQueueOrganizationJobsJobConnectionPageInfo {\n\treturn v.PageInfo\n}\n\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge includes the requested fields of the GraphQL type JobEdge.\ntype ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge struct {\n\tNode *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob `json:\"-\"`\n}\n\n// GetNode returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge.Node, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge) GetNode() *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob {\n\treturn v.Node\n}\n\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge) UnmarshalJSON(b []byte) error {\n\n\tif string(b) == \"null\" {\n\t\treturn nil\n\t}\n\n\tvar firstPass struct {\n\t\t*ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge\n\t\tNode json.RawMessage `json:\"node\"`\n\t\tgraphql.NoUnmarshalJSON\n\t}\n\tfirstPass.ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge = v\n\n\terr := json.Unmarshal(b, &firstPass)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t{\n\t\tdst := &v.Node\n\t\tsrc := firstPass.Node\n\t\tif len(src) != 0 && string(src) != \"null\" {\n\t\t\t*dst = new(ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob)\n\t\t\terr = __unmarshalListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(\n\t\t\t\tsrc, *dst)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\"unable to unmarshal ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge.Node: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\ntype __premarshalListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge struct {\n\tNode json.RawMessage `json:\"node\"`\n}\n\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge) MarshalJSON() ([]byte, error) {\n\tpremarshaled, err := v.__premarshalJSON()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn json.Marshal(premarshaled)\n}\n\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge) __premarshalJSON() (*__premarshalListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge, error) {\n\tvar retval __premarshalListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge\n\n\t{\n\n\t\tdst := &retval.Node\n\t\tsrc := v.Node\n\t\tif src != nil {\n\t\t\tvar err error\n\t\t\t*dst, err = __marshalListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(\n\t\t\t\tsrc)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\t\"unable to marshal ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdge.Node: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn &retval, nil\n}\n\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob includes the requested fields of the GraphQL interface Job.\n//\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob is implemented by the following types:\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait\n// The GraphQL type's documentation follows.\n//\n// Kinds of jobs that can exist on a build\ntype ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob interface {\n\timplementsGraphQLInterfaceListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob()\n\t// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).\n\tGetTypename() *string\n}\n\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock) implementsGraphQLInterfaceListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) implementsGraphQLInterfaceListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger) implementsGraphQLInterfaceListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait) implementsGraphQLInterfaceListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\n\nfunc __unmarshalListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(b []byte, v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob) error {\n\tif string(b) == \"null\" {\n\t\treturn nil\n\t}\n\n\tvar tn struct {\n\t\tTypeName string `json:\"__typename\"`\n\t}\n\terr := json.Unmarshal(b, &tn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch tn.TypeName {\n\tcase \"JobTypeBlock\":\n\t\t*v = new(ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeCommand\":\n\t\t*v = new(ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeTrigger\":\n\t\t*v = new(ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeWait\":\n\t\t*v = new(ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"\":\n\t\treturn fmt.Errorf(\n\t\t\t\"response was missing Job.__typename\")\n\tdefault:\n\t\treturn fmt.Errorf(\n\t\t\t`unexpected concrete type for ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob: \"%v\"`, tn.TypeName)\n\t}\n}\n\nfunc __marshalListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob) ([]byte, error) {\n\n\tvar typename string\n\tswitch v := (*v).(type) {\n\tcase *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock:\n\t\ttypename = \"JobTypeBlock\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand:\n\t\ttypename = \"JobTypeCommand\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger:\n\t\ttypename = \"JobTypeTrigger\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait:\n\t\ttypename = \"JobTypeWait\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase nil:\n\t\treturn []byte(\"null\"), nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\n\t\t\t`unexpected concrete type for ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJob: \"%T\"`, v)\n\t}\n}\n\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock includes the requested fields of the GraphQL type JobTypeBlock.\n// The GraphQL type's documentation follows.\n//\n// A type of job that requires a user to unblock it before proceeding in a build pipeline\ntype ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock) GetTypename() *string {\n\treturn v.Typename\n}\n\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand includes the requested fields of the GraphQL type JobTypeCommand.\n// The GraphQL type's documentation follows.\n//\n// A type of job that runs a command on an agent\ntype ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand struct {\n\tTypename *string `json:\"__typename\"`\n\tId       string  `json:\"id\"`\n\t// The UUID for this job\n\tUuid string `json:\"uuid\"`\n\t// The command the job will run\n\tCommand *string `json:\"command\"`\n\t// The state of the job\n\tState JobStates `json:\"state\"`\n\t// The exit status returned by the command on the agent\n\tExitStatus *string `json:\"exitStatus\"`\n\t// The URL for the job\n\tUrl string `json:\"url\"`\n\t// The time when the job started running\n\tStartedAt *time.Time `json:\"startedAt\"`\n\t// The time when the job finished\n\tFinishedAt *time.Time `json:\"finishedAt\"`\n\t// The time when the job was created\n\tCreatedAt *time.Time `json:\"createdAt\"`\n\t// The cluster of this job\n\tCluster *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster `json:\"cluster\"`\n\t// The cluster queue of this job\n\tClusterQueue *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue `json:\"clusterQueue\"`\n\t// The agent that is running the job\n\tAgent *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent `json:\"agent\"`\n}\n\n// GetTypename returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetTypename() *string {\n\treturn v.Typename\n}\n\n// GetId returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Id, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetId() string {\n\treturn v.Id\n}\n\n// GetUuid returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Uuid, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetUuid() string {\n\treturn v.Uuid\n}\n\n// GetCommand returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Command, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetCommand() *string {\n\treturn v.Command\n}\n\n// GetState returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.State, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetState() JobStates {\n\treturn v.State\n}\n\n// GetExitStatus returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.ExitStatus, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetExitStatus() *string {\n\treturn v.ExitStatus\n}\n\n// GetUrl returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Url, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetUrl() string {\n\treturn v.Url\n}\n\n// GetStartedAt returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.StartedAt, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetStartedAt() *time.Time {\n\treturn v.StartedAt\n}\n\n// GetFinishedAt returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.FinishedAt, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetFinishedAt() *time.Time {\n\treturn v.FinishedAt\n}\n\n// GetCreatedAt returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.CreatedAt, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetCreatedAt() *time.Time {\n\treturn v.CreatedAt\n}\n\n// GetCluster returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Cluster, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetCluster() *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster {\n\treturn v.Cluster\n}\n\n// GetClusterQueue returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.ClusterQueue, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetClusterQueue() *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue {\n\treturn v.ClusterQueue\n}\n\n// GetAgent returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Agent, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetAgent() *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent {\n\treturn v.Agent\n}\n\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent includes the requested fields of the GraphQL type Agent.\n// The GraphQL type's documentation follows.\n//\n// An agent\ntype ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent struct {\n\tId string `json:\"id\"`\n\t// The name of the agent\n\tName string `json:\"name\"`\n\t// The hostname of the machine running the agent\n\tHostname *string `json:\"hostname\"`\n\t// The meta data this agent was stared with\n\tMetaData []string `json:\"metaData\"`\n}\n\n// GetId returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.Id, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetId() string {\n\treturn v.Id\n}\n\n// GetName returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.Name, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetName() string {\n\treturn v.Name\n}\n\n// GetHostname returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.Hostname, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetHostname() *string {\n\treturn v.Hostname\n}\n\n// GetMetaData returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.MetaData, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetMetaData() []string {\n\treturn v.MetaData\n}\n\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster includes the requested fields of the GraphQL type Cluster.\ntype ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster struct {\n\tId string `json:\"id\"`\n\t// Name of the cluster\n\tName string `json:\"name\"`\n}\n\n// GetId returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster.Id, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster) GetId() string {\n\treturn v.Id\n}\n\n// GetName returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster.Name, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster) GetName() string {\n\treturn v.Name\n}\n\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue includes the requested fields of the GraphQL type ClusterQueue.\ntype ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue struct {\n\tId  string `json:\"id\"`\n\tKey string `json:\"key\"`\n}\n\n// GetId returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue.Id, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue) GetId() string {\n\treturn v.Id\n}\n\n// GetKey returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue.Key, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue) GetKey() string {\n\treturn v.Key\n}\n\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger includes the requested fields of the GraphQL type JobTypeTrigger.\n// The GraphQL type's documentation follows.\n//\n// A type of job that triggers another build on a pipeline\ntype ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger) GetTypename() *string {\n\treturn v.Typename\n}\n\n// ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait includes the requested fields of the GraphQL type JobTypeWait.\n// The GraphQL type's documentation follows.\n//\n// A type of job that waits for all previous jobs to pass before proceeding the build pipeline\ntype ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait) GetTypename() *string {\n\treturn v.Typename\n}\n\n// ListJobsByQueueOrganizationJobsJobConnectionPageInfo includes the requested fields of the GraphQL type PageInfo.\n// The GraphQL type's documentation follows.\n//\n// Information about pagination in a connection.\ntype ListJobsByQueueOrganizationJobsJobConnectionPageInfo struct {\n\t// When paginating forwards, the cursor to continue.\n\tEndCursor *string `json:\"endCursor\"`\n\t// When paginating forwards, are there more items?\n\tHasNextPage bool `json:\"hasNextPage\"`\n}\n\n// GetEndCursor returns ListJobsByQueueOrganizationJobsJobConnectionPageInfo.EndCursor, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionPageInfo) GetEndCursor() *string {\n\treturn v.EndCursor\n}\n\n// GetHasNextPage returns ListJobsByQueueOrganizationJobsJobConnectionPageInfo.HasNextPage, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueOrganizationJobsJobConnectionPageInfo) GetHasNextPage() bool {\n\treturn v.HasNextPage\n}\n\n// ListJobsByQueueResponse is returned by ListJobsByQueue on success.\ntype ListJobsByQueueResponse struct {\n\t// Find an organization\n\tOrganization *ListJobsByQueueOrganization `json:\"organization\"`\n}\n\n// GetOrganization returns ListJobsByQueueResponse.Organization, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByQueueResponse) GetOrganization() *ListJobsByQueueOrganization {\n\treturn v.Organization\n}\n\n// ListJobsByStateOrganization includes the requested fields of the GraphQL type Organization.\n// The GraphQL type's documentation follows.\n//\n// An organization\ntype ListJobsByStateOrganization struct {\n\tJobs *ListJobsByStateOrganizationJobsJobConnection `json:\"jobs\"`\n}\n\n// GetJobs returns ListJobsByStateOrganization.Jobs, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganization) GetJobs() *ListJobsByStateOrganizationJobsJobConnection {\n\treturn v.Jobs\n}\n\n// ListJobsByStateOrganizationJobsJobConnection includes the requested fields of the GraphQL type JobConnection.\ntype ListJobsByStateOrganizationJobsJobConnection struct {\n\tEdges    []*ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge `json:\"edges\"`\n\tPageInfo *ListJobsByStateOrganizationJobsJobConnectionPageInfo       `json:\"pageInfo\"`\n}\n\n// GetEdges returns ListJobsByStateOrganizationJobsJobConnection.Edges, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnection) GetEdges() []*ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge {\n\treturn v.Edges\n}\n\n// GetPageInfo returns ListJobsByStateOrganizationJobsJobConnection.PageInfo, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnection) GetPageInfo() *ListJobsByStateOrganizationJobsJobConnectionPageInfo {\n\treturn v.PageInfo\n}\n\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge includes the requested fields of the GraphQL type JobEdge.\ntype ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge struct {\n\tNode *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob `json:\"-\"`\n}\n\n// GetNode returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge.Node, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge) GetNode() *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob {\n\treturn v.Node\n}\n\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge) UnmarshalJSON(b []byte) error {\n\n\tif string(b) == \"null\" {\n\t\treturn nil\n\t}\n\n\tvar firstPass struct {\n\t\t*ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge\n\t\tNode json.RawMessage `json:\"node\"`\n\t\tgraphql.NoUnmarshalJSON\n\t}\n\tfirstPass.ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge = v\n\n\terr := json.Unmarshal(b, &firstPass)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t{\n\t\tdst := &v.Node\n\t\tsrc := firstPass.Node\n\t\tif len(src) != 0 && string(src) != \"null\" {\n\t\t\t*dst = new(ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob)\n\t\t\terr = __unmarshalListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(\n\t\t\t\tsrc, *dst)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\n\t\t\t\t\t\"unable to unmarshal ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge.Node: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\ntype __premarshalListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge struct {\n\tNode json.RawMessage `json:\"node\"`\n}\n\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge) MarshalJSON() ([]byte, error) {\n\tpremarshaled, err := v.__premarshalJSON()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn json.Marshal(premarshaled)\n}\n\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge) __premarshalJSON() (*__premarshalListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge, error) {\n\tvar retval __premarshalListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge\n\n\t{\n\n\t\tdst := &retval.Node\n\t\tsrc := v.Node\n\t\tif src != nil {\n\t\t\tvar err error\n\t\t\t*dst, err = __marshalListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(\n\t\t\t\tsrc)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\t\"unable to marshal ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdge.Node: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\treturn &retval, nil\n}\n\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob includes the requested fields of the GraphQL interface Job.\n//\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob is implemented by the following types:\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait\n// The GraphQL type's documentation follows.\n//\n// Kinds of jobs that can exist on a build\ntype ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob interface {\n\timplementsGraphQLInterfaceListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob()\n\t// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).\n\tGetTypename() *string\n}\n\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock) implementsGraphQLInterfaceListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) implementsGraphQLInterfaceListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger) implementsGraphQLInterfaceListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait) implementsGraphQLInterfaceListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob() {\n}\n\nfunc __unmarshalListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(b []byte, v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob) error {\n\tif string(b) == \"null\" {\n\t\treturn nil\n\t}\n\n\tvar tn struct {\n\t\tTypeName string `json:\"__typename\"`\n\t}\n\terr := json.Unmarshal(b, &tn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch tn.TypeName {\n\tcase \"JobTypeBlock\":\n\t\t*v = new(ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeCommand\":\n\t\t*v = new(ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeTrigger\":\n\t\t*v = new(ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"JobTypeWait\":\n\t\t*v = new(ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait)\n\t\treturn json.Unmarshal(b, *v)\n\tcase \"\":\n\t\treturn fmt.Errorf(\n\t\t\t\"response was missing Job.__typename\")\n\tdefault:\n\t\treturn fmt.Errorf(\n\t\t\t`unexpected concrete type for ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob: \"%v\"`, tn.TypeName)\n\t}\n}\n\nfunc __marshalListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob(v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob) ([]byte, error) {\n\n\tvar typename string\n\tswitch v := (*v).(type) {\n\tcase *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock:\n\t\ttypename = \"JobTypeBlock\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand:\n\t\ttypename = \"JobTypeCommand\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger:\n\t\ttypename = \"JobTypeTrigger\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait:\n\t\ttypename = \"JobTypeWait\"\n\n\t\tresult := struct {\n\t\t\tTypeName string `json:\"__typename\"`\n\t\t\t*ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait\n\t\t}{typename, v}\n\t\treturn json.Marshal(result)\n\tcase nil:\n\t\treturn []byte(\"null\"), nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\n\t\t\t`unexpected concrete type for ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJob: \"%T\"`, v)\n\t}\n}\n\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock includes the requested fields of the GraphQL type JobTypeBlock.\n// The GraphQL type's documentation follows.\n//\n// A type of job that requires a user to unblock it before proceeding in a build pipeline\ntype ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeBlock) GetTypename() *string {\n\treturn v.Typename\n}\n\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand includes the requested fields of the GraphQL type JobTypeCommand.\n// The GraphQL type's documentation follows.\n//\n// A type of job that runs a command on an agent\ntype ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand struct {\n\tTypename *string `json:\"__typename\"`\n\tId       string  `json:\"id\"`\n\t// The UUID for this job\n\tUuid string `json:\"uuid\"`\n\t// The label of the job\n\tLabel *string `json:\"label\"`\n\t// The command the job will run\n\tCommand *string `json:\"command\"`\n\t// The state of the job\n\tState JobStates `json:\"state\"`\n\t// The exit status returned by the command on the agent\n\tExitStatus *string `json:\"exitStatus\"`\n\t// The URL for the job\n\tUrl string `json:\"url\"`\n\t// The time when the job started running\n\tStartedAt *time.Time `json:\"startedAt\"`\n\t// The time when the job finished\n\tFinishedAt *time.Time `json:\"finishedAt\"`\n\t// The time when the job was created\n\tCreatedAt *time.Time `json:\"createdAt\"`\n\t// The cluster of this job\n\tCluster *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster `json:\"cluster\"`\n\t// The cluster queue of this job\n\tClusterQueue *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue `json:\"clusterQueue\"`\n\t// The agent that is running the job\n\tAgent *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent `json:\"agent\"`\n}\n\n// GetTypename returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetTypename() *string {\n\treturn v.Typename\n}\n\n// GetId returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Id, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetId() string {\n\treturn v.Id\n}\n\n// GetUuid returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Uuid, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetUuid() string {\n\treturn v.Uuid\n}\n\n// GetLabel returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Label, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetLabel() *string {\n\treturn v.Label\n}\n\n// GetCommand returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Command, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetCommand() *string {\n\treturn v.Command\n}\n\n// GetState returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.State, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetState() JobStates {\n\treturn v.State\n}\n\n// GetExitStatus returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.ExitStatus, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetExitStatus() *string {\n\treturn v.ExitStatus\n}\n\n// GetUrl returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Url, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetUrl() string {\n\treturn v.Url\n}\n\n// GetStartedAt returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.StartedAt, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetStartedAt() *time.Time {\n\treturn v.StartedAt\n}\n\n// GetFinishedAt returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.FinishedAt, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetFinishedAt() *time.Time {\n\treturn v.FinishedAt\n}\n\n// GetCreatedAt returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.CreatedAt, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetCreatedAt() *time.Time {\n\treturn v.CreatedAt\n}\n\n// GetCluster returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Cluster, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetCluster() *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster {\n\treturn v.Cluster\n}\n\n// GetClusterQueue returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.ClusterQueue, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetClusterQueue() *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue {\n\treturn v.ClusterQueue\n}\n\n// GetAgent returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand.Agent, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommand) GetAgent() *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent {\n\treturn v.Agent\n}\n\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent includes the requested fields of the GraphQL type Agent.\n// The GraphQL type's documentation follows.\n//\n// An agent\ntype ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent struct {\n\tId string `json:\"id\"`\n\t// The name of the agent\n\tName string `json:\"name\"`\n\t// The hostname of the machine running the agent\n\tHostname *string `json:\"hostname\"`\n\t// The meta data this agent was stared with\n\tMetaData []string `json:\"metaData\"`\n}\n\n// GetId returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.Id, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetId() string {\n\treturn v.Id\n}\n\n// GetName returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.Name, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetName() string {\n\treturn v.Name\n}\n\n// GetHostname returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.Hostname, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetHostname() *string {\n\treturn v.Hostname\n}\n\n// GetMetaData returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent.MetaData, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandAgent) GetMetaData() []string {\n\treturn v.MetaData\n}\n\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster includes the requested fields of the GraphQL type Cluster.\ntype ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster struct {\n\tId string `json:\"id\"`\n\t// Name of the cluster\n\tName string `json:\"name\"`\n}\n\n// GetId returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster.Id, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster) GetId() string {\n\treturn v.Id\n}\n\n// GetName returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster.Name, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandCluster) GetName() string {\n\treturn v.Name\n}\n\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue includes the requested fields of the GraphQL type ClusterQueue.\ntype ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue struct {\n\tId  string `json:\"id\"`\n\tKey string `json:\"key\"`\n}\n\n// GetId returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue.Id, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue) GetId() string {\n\treturn v.Id\n}\n\n// GetKey returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue.Key, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeCommandClusterQueue) GetKey() string {\n\treturn v.Key\n}\n\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger includes the requested fields of the GraphQL type JobTypeTrigger.\n// The GraphQL type's documentation follows.\n//\n// A type of job that triggers another build on a pipeline\ntype ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeTrigger) GetTypename() *string {\n\treturn v.Typename\n}\n\n// ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait includes the requested fields of the GraphQL type JobTypeWait.\n// The GraphQL type's documentation follows.\n//\n// A type of job that waits for all previous jobs to pass before proceeding the build pipeline\ntype ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait struct {\n\tTypename *string `json:\"__typename\"`\n}\n\n// GetTypename returns ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait.Typename, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionEdgesJobEdgeNodeJobTypeWait) GetTypename() *string {\n\treturn v.Typename\n}\n\n// ListJobsByStateOrganizationJobsJobConnectionPageInfo includes the requested fields of the GraphQL type PageInfo.\n// The GraphQL type's documentation follows.\n//\n// Information about pagination in a connection.\ntype ListJobsByStateOrganizationJobsJobConnectionPageInfo struct {\n\t// When paginating forwards, the cursor to continue.\n\tEndCursor *string `json:\"endCursor\"`\n\t// When paginating forwards, are there more items?\n\tHasNextPage bool `json:\"hasNextPage\"`\n}\n\n// GetEndCursor returns ListJobsByStateOrganizationJobsJobConnectionPageInfo.EndCursor, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionPageInfo) GetEndCursor() *string {\n\treturn v.EndCursor\n}\n\n// GetHasNextPage returns ListJobsByStateOrganizationJobsJobConnectionPageInfo.HasNextPage, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateOrganizationJobsJobConnectionPageInfo) GetHasNextPage() bool {\n\treturn v.HasNextPage\n}\n\n// ListJobsByStateResponse is returned by ListJobsByState on success.\ntype ListJobsByStateResponse struct {\n\t// Find an organization\n\tOrganization *ListJobsByStateOrganization `json:\"organization\"`\n}\n\n// GetOrganization returns ListJobsByStateResponse.Organization, and is useful for accessing the field via an interface.\nfunc (v *ListJobsByStateResponse) GetOrganization() *ListJobsByStateOrganization {\n\treturn v.Organization\n}\n\n// PipelineCreateWebhookPipelineCreateWebhookPipelineCreateWebhookPayload includes the requested fields of the GraphQL type PipelineCreateWebhookPayload.\n// The GraphQL type's documentation follows.\n//\n// Autogenerated return type of PipelineCreateWebhook.\ntype PipelineCreateWebhookPipelineCreateWebhookPipelineCreateWebhookPayload struct {\n\t// A unique identifier for the client performing the mutation.\n\tClientMutationId *string `json:\"clientMutationId\"`\n\tPipelineID       string  `json:\"pipelineID\"`\n}\n\n// GetClientMutationId returns PipelineCreateWebhookPipelineCreateWebhookPipelineCreateWebhookPayload.ClientMutationId, and is useful for accessing the field via an interface.\nfunc (v *PipelineCreateWebhookPipelineCreateWebhookPipelineCreateWebhookPayload) GetClientMutationId() *string {\n\treturn v.ClientMutationId\n}\n\n// GetPipelineID returns PipelineCreateWebhookPipelineCreateWebhookPipelineCreateWebhookPayload.PipelineID, and is useful for accessing the field via an interface.\nfunc (v *PipelineCreateWebhookPipelineCreateWebhookPipelineCreateWebhookPayload) GetPipelineID() string {\n\treturn v.PipelineID\n}\n\n// PipelineCreateWebhookResponse is returned by PipelineCreateWebhook on success.\ntype PipelineCreateWebhookResponse struct {\n\t// Create SCM webhooks for a pipeline.\n\tPipelineCreateWebhook *PipelineCreateWebhookPipelineCreateWebhookPipelineCreateWebhookPayload `json:\"pipelineCreateWebhook\"`\n}\n\n// GetPipelineCreateWebhook returns PipelineCreateWebhookResponse.PipelineCreateWebhook, and is useful for accessing the field via an interface.\nfunc (v *PipelineCreateWebhookResponse) GetPipelineCreateWebhook() *PipelineCreateWebhookPipelineCreateWebhookPipelineCreateWebhookPayload {\n\treturn v.PipelineCreateWebhook\n}\n\n// RetryJobJobTypeCommandRetryJobTypeCommandRetryPayload includes the requested fields of the GraphQL type JobTypeCommandRetryPayload.\n// The GraphQL type's documentation follows.\n//\n// Autogenerated return type of JobTypeCommandRetry.\ntype RetryJobJobTypeCommandRetryJobTypeCommandRetryPayload struct {\n\tJobTypeCommand RetryJobJobTypeCommandRetryJobTypeCommandRetryPayloadJobTypeCommand `json:\"jobTypeCommand\"`\n}\n\n// GetJobTypeCommand returns RetryJobJobTypeCommandRetryJobTypeCommandRetryPayload.JobTypeCommand, and is useful for accessing the field via an interface.\nfunc (v *RetryJobJobTypeCommandRetryJobTypeCommandRetryPayload) GetJobTypeCommand() RetryJobJobTypeCommandRetryJobTypeCommandRetryPayloadJobTypeCommand {\n\treturn v.JobTypeCommand\n}\n\n// RetryJobJobTypeCommandRetryJobTypeCommandRetryPayloadJobTypeCommand includes the requested fields of the GraphQL type JobTypeCommand.\n// The GraphQL type's documentation follows.\n//\n// A type of job that runs a command on an agent\ntype RetryJobJobTypeCommandRetryJobTypeCommandRetryPayloadJobTypeCommand struct {\n\tId string `json:\"id\"`\n\t// The state of the job\n\tState JobStates `json:\"state\"`\n\t// The URL for the job\n\tUrl string `json:\"url\"`\n}\n\n// GetId returns RetryJobJobTypeCommandRetryJobTypeCommandRetryPayloadJobTypeCommand.Id, and is useful for accessing the field via an interface.\nfunc (v *RetryJobJobTypeCommandRetryJobTypeCommandRetryPayloadJobTypeCommand) GetId() string {\n\treturn v.Id\n}\n\n// GetState returns RetryJobJobTypeCommandRetryJobTypeCommandRetryPayloadJobTypeCommand.State, and is useful for accessing the field via an interface.\nfunc (v *RetryJobJobTypeCommandRetryJobTypeCommandRetryPayloadJobTypeCommand) GetState() JobStates {\n\treturn v.State\n}\n\n// GetUrl returns RetryJobJobTypeCommandRetryJobTypeCommandRetryPayloadJobTypeCommand.Url, and is useful for accessing the field via an interface.\nfunc (v *RetryJobJobTypeCommandRetryJobTypeCommandRetryPayloadJobTypeCommand) GetUrl() string {\n\treturn v.Url\n}\n\n// RetryJobResponse is returned by RetryJob on success.\ntype RetryJobResponse struct {\n\t// Retry a job.\n\tJobTypeCommandRetry *RetryJobJobTypeCommandRetryJobTypeCommandRetryPayload `json:\"jobTypeCommandRetry\"`\n}\n\n// GetJobTypeCommandRetry returns RetryJobResponse.JobTypeCommandRetry, and is useful for accessing the field via an interface.\nfunc (v *RetryJobResponse) GetJobTypeCommandRetry() *RetryJobJobTypeCommandRetryJobTypeCommandRetryPayload {\n\treturn v.JobTypeCommandRetry\n}\n\n// UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayload includes the requested fields of the GraphQL type JobTypeBlockUnblockPayload.\n// The GraphQL type's documentation follows.\n//\n// Autogenerated return type of JobTypeBlockUnblock.\ntype UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayload struct {\n\tJobTypeBlock UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock `json:\"jobTypeBlock\"`\n}\n\n// GetJobTypeBlock returns UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayload.JobTypeBlock, and is useful for accessing the field via an interface.\nfunc (v *UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayload) GetJobTypeBlock() UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock {\n\treturn v.JobTypeBlock\n}\n\n// UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock includes the requested fields of the GraphQL type JobTypeBlock.\n// The GraphQL type's documentation follows.\n//\n// A type of job that requires a user to unblock it before proceeding in a build pipeline\ntype UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock struct {\n\tId string `json:\"id\"`\n\t// The state of the job\n\tState JobStates `json:\"state\"`\n\t// Whether or not this job can be unblocked yet (may be waiting on another job to finish)\n\tIsUnblockable *bool `json:\"isUnblockable\"`\n\t// The build that this job is a part of\n\tBuild *UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlockBuild `json:\"build\"`\n}\n\n// GetId returns UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock.Id, and is useful for accessing the field via an interface.\nfunc (v *UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock) GetId() string {\n\treturn v.Id\n}\n\n// GetState returns UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock.State, and is useful for accessing the field via an interface.\nfunc (v *UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock) GetState() JobStates {\n\treturn v.State\n}\n\n// GetIsUnblockable returns UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock.IsUnblockable, and is useful for accessing the field via an interface.\nfunc (v *UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock) GetIsUnblockable() *bool {\n\treturn v.IsUnblockable\n}\n\n// GetBuild returns UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock.Build, and is useful for accessing the field via an interface.\nfunc (v *UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlock) GetBuild() *UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlockBuild {\n\treturn v.Build\n}\n\n// UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlockBuild includes the requested fields of the GraphQL type Build.\n// The GraphQL type's documentation follows.\n//\n// A build from a pipeline\ntype UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlockBuild struct {\n\t// The URL for the build\n\tUrl string `json:\"url\"`\n}\n\n// GetUrl returns UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlockBuild.Url, and is useful for accessing the field via an interface.\nfunc (v *UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayloadJobTypeBlockBuild) GetUrl() string {\n\treturn v.Url\n}\n\n// UnblockJobResponse is returned by UnblockJob on success.\ntype UnblockJobResponse struct {\n\t// Unblocks a build's \"Block pipeline\" job.\n\tJobTypeBlockUnblock *UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayload `json:\"jobTypeBlockUnblock\"`\n}\n\n// GetJobTypeBlockUnblock returns UnblockJobResponse.JobTypeBlockUnblock, and is useful for accessing the field via an interface.\nfunc (v *UnblockJobResponse) GetJobTypeBlockUnblock() *UnblockJobJobTypeBlockUnblockJobTypeBlockUnblockPayload {\n\treturn v.JobTypeBlockUnblock\n}\n\n// __CancelJobInput is used internally by genqlient\ntype __CancelJobInput struct {\n\tJobId string `json:\"jobId\"`\n}\n\n// GetJobId returns __CancelJobInput.JobId, and is useful for accessing the field via an interface.\nfunc (v *__CancelJobInput) GetJobId() string { return v.JobId }\n\n// __FindClustersInput is used internally by genqlient\ntype __FindClustersInput struct {\n\tOrg    string  `json:\"org\"`\n\tCursor *string `json:\"cursor\"`\n}\n\n// GetOrg returns __FindClustersInput.Org, and is useful for accessing the field via an interface.\nfunc (v *__FindClustersInput) GetOrg() string { return v.Org }\n\n// GetCursor returns __FindClustersInput.Cursor, and is useful for accessing the field via an interface.\nfunc (v *__FindClustersInput) GetCursor() *string { return v.Cursor }\n\n// __FindQueuesForClusterInput is used internally by genqlient\ntype __FindQueuesForClusterInput struct {\n\tClusterId string  `json:\"clusterId\"`\n\tCursor    *string `json:\"cursor\"`\n}\n\n// GetClusterId returns __FindQueuesForClusterInput.ClusterId, and is useful for accessing the field via an interface.\nfunc (v *__FindQueuesForClusterInput) GetClusterId() string { return v.ClusterId }\n\n// GetCursor returns __FindQueuesForClusterInput.Cursor, and is useful for accessing the field via an interface.\nfunc (v *__FindQueuesForClusterInput) GetCursor() *string { return v.Cursor }\n\n// __FindUserByEmailInput is used internally by genqlient\ntype __FindUserByEmailInput struct {\n\tOrganization string `json:\"organization\"`\n\tEmail        string `json:\"email\"`\n}\n\n// GetOrganization returns __FindUserByEmailInput.Organization, and is useful for accessing the field via an interface.\nfunc (v *__FindUserByEmailInput) GetOrganization() string { return v.Organization }\n\n// GetEmail returns __FindUserByEmailInput.Email, and is useful for accessing the field via an interface.\nfunc (v *__FindUserByEmailInput) GetEmail() string { return v.Email }\n\n// __GetClusterQueueAgentInput is used internally by genqlient\ntype __GetClusterQueueAgentInput struct {\n\tOrgSlug string   `json:\"orgSlug\"`\n\tQueueId []string `json:\"queueId\"`\n}\n\n// GetOrgSlug returns __GetClusterQueueAgentInput.OrgSlug, and is useful for accessing the field via an interface.\nfunc (v *__GetClusterQueueAgentInput) GetOrgSlug() string { return v.OrgSlug }\n\n// GetQueueId returns __GetClusterQueueAgentInput.QueueId, and is useful for accessing the field via an interface.\nfunc (v *__GetClusterQueueAgentInput) GetQueueId() []string { return v.QueueId }\n\n// __GetClusterQueuesInput is used internally by genqlient\ntype __GetClusterQueuesInput struct {\n\tOrgSlug   string `json:\"orgSlug\"`\n\tClusterId string `json:\"clusterId\"`\n}\n\n// GetOrgSlug returns __GetClusterQueuesInput.OrgSlug, and is useful for accessing the field via an interface.\nfunc (v *__GetClusterQueuesInput) GetOrgSlug() string { return v.OrgSlug }\n\n// GetClusterId returns __GetClusterQueuesInput.ClusterId, and is useful for accessing the field via an interface.\nfunc (v *__GetClusterQueuesInput) GetClusterId() string { return v.ClusterId }\n\n// __GetOrganizationIDInput is used internally by genqlient\ntype __GetOrganizationIDInput struct {\n\tSlug string `json:\"slug\"`\n}\n\n// GetSlug returns __GetOrganizationIDInput.Slug, and is useful for accessing the field via an interface.\nfunc (v *__GetOrganizationIDInput) GetSlug() string { return v.Slug }\n\n// __InviteUserInput is used internally by genqlient\ntype __InviteUserInput struct {\n\tOrganization string   `json:\"organization\"`\n\tEmails       []string `json:\"emails\"`\n}\n\n// GetOrganization returns __InviteUserInput.Organization, and is useful for accessing the field via an interface.\nfunc (v *__InviteUserInput) GetOrganization() string { return v.Organization }\n\n// GetEmails returns __InviteUserInput.Emails, and is useful for accessing the field via an interface.\nfunc (v *__InviteUserInput) GetEmails() []string { return v.Emails }\n\n// __ListJobsByAgentQueryRulesInput is used internally by genqlient\ntype __ListJobsByAgentQueryRulesInput struct {\n\tOrg             string   `json:\"org\"`\n\tAgentQueryRules []string `json:\"agentQueryRules\"`\n\tFirst           *int     `json:\"first\"`\n\tAfter           *string  `json:\"after\"`\n}\n\n// GetOrg returns __ListJobsByAgentQueryRulesInput.Org, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByAgentQueryRulesInput) GetOrg() string { return v.Org }\n\n// GetAgentQueryRules returns __ListJobsByAgentQueryRulesInput.AgentQueryRules, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByAgentQueryRulesInput) GetAgentQueryRules() []string { return v.AgentQueryRules }\n\n// GetFirst returns __ListJobsByAgentQueryRulesInput.First, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByAgentQueryRulesInput) GetFirst() *int { return v.First }\n\n// GetAfter returns __ListJobsByAgentQueryRulesInput.After, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByAgentQueryRulesInput) GetAfter() *string { return v.After }\n\n// __ListJobsByQueueInput is used internally by genqlient\ntype __ListJobsByQueueInput struct {\n\tOrg          string   `json:\"org\"`\n\tClusterQueue []string `json:\"clusterQueue\"`\n\tFirst        *int     `json:\"first\"`\n\tAfter        *string  `json:\"after\"`\n}\n\n// GetOrg returns __ListJobsByQueueInput.Org, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByQueueInput) GetOrg() string { return v.Org }\n\n// GetClusterQueue returns __ListJobsByQueueInput.ClusterQueue, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByQueueInput) GetClusterQueue() []string { return v.ClusterQueue }\n\n// GetFirst returns __ListJobsByQueueInput.First, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByQueueInput) GetFirst() *int { return v.First }\n\n// GetAfter returns __ListJobsByQueueInput.After, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByQueueInput) GetAfter() *string { return v.After }\n\n// __ListJobsByStateInput is used internally by genqlient\ntype __ListJobsByStateInput struct {\n\tOrg   string      `json:\"org\"`\n\tState []JobStates `json:\"state\"`\n\tFirst *int        `json:\"first\"`\n\tAfter *string     `json:\"after\"`\n}\n\n// GetOrg returns __ListJobsByStateInput.Org, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByStateInput) GetOrg() string { return v.Org }\n\n// GetState returns __ListJobsByStateInput.State, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByStateInput) GetState() []JobStates { return v.State }\n\n// GetFirst returns __ListJobsByStateInput.First, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByStateInput) GetFirst() *int { return v.First }\n\n// GetAfter returns __ListJobsByStateInput.After, and is useful for accessing the field via an interface.\nfunc (v *__ListJobsByStateInput) GetAfter() *string { return v.After }\n\n// __PipelineCreateWebhookInput is used internally by genqlient\ntype __PipelineCreateWebhookInput struct {\n\tId string `json:\"id\"`\n}\n\n// GetId returns __PipelineCreateWebhookInput.Id, and is useful for accessing the field via an interface.\nfunc (v *__PipelineCreateWebhookInput) GetId() string { return v.Id }\n\n// __RetryJobInput is used internally by genqlient\ntype __RetryJobInput struct {\n\tId string `json:\"id\"`\n}\n\n// GetId returns __RetryJobInput.Id, and is useful for accessing the field via an interface.\nfunc (v *__RetryJobInput) GetId() string { return v.Id }\n\n// __UnblockJobInput is used internally by genqlient\ntype __UnblockJobInput struct {\n\tId     string  `json:\"id\"`\n\tFields *string `json:\"fields\"`\n}\n\n// GetId returns __UnblockJobInput.Id, and is useful for accessing the field via an interface.\nfunc (v *__UnblockJobInput) GetId() string { return v.Id }\n\n// GetFields returns __UnblockJobInput.Fields, and is useful for accessing the field via an interface.\nfunc (v *__UnblockJobInput) GetFields() *string { return v.Fields }\n\n// The mutation executed by CancelJob.\nconst CancelJob_Operation = `\nmutation CancelJob ($jobId: ID!) {\n\tjobTypeCommandCancel(input: {id:$jobId}) {\n\t\tclientMutationId\n\t\tjobTypeCommand {\n\t\t\tid\n\t\t\tuuid\n\t\t\tstate\n\t\t\turl\n\t\t}\n\t}\n}\n`\n\nfunc CancelJob(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\tjobId string,\n) (data_ *CancelJobResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"CancelJob\",\n\t\tQuery:  CancelJob_Operation,\n\t\tVariables: &__CancelJobInput{\n\t\t\tJobId: jobId,\n\t\t},\n\t}\n\n\tdata_ = &CancelJobResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The query executed by FindClusters.\nconst FindClusters_Operation = `\nquery FindClusters ($org: ID!, $cursor: String) {\n\torganization(slug: $org) {\n\t\tclusters(first: 100, after: $cursor) {\n\t\t\tedges {\n\t\t\t\tnode {\n\t\t\t\t\tid\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t}\n\t\t\tpageInfo {\n\t\t\t\thasNextPage\n\t\t\t\tendCursor\n\t\t\t}\n\t\t}\n\t}\n}\n`\n\nfunc FindClusters(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\torg string,\n\tcursor *string,\n) (data_ *FindClustersResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"FindClusters\",\n\t\tQuery:  FindClusters_Operation,\n\t\tVariables: &__FindClustersInput{\n\t\t\tOrg:    org,\n\t\t\tCursor: cursor,\n\t\t},\n\t}\n\n\tdata_ = &FindClustersResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The query executed by FindQueuesForCluster.\nconst FindQueuesForCluster_Operation = `\nquery FindQueuesForCluster ($clusterId: ID!, $cursor: String) {\n\tnode(id: $clusterId) {\n\t\t__typename\n\t\t... on Cluster {\n\t\t\tid\n\t\t\tname\n\t\t\tqueues(first: 100, after: $cursor) {\n\t\t\t\tedges {\n\t\t\t\t\tnode {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tkey\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tpageInfo {\n\t\t\t\t\thasNextPage\n\t\t\t\t\tendCursor\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n`\n\nfunc FindQueuesForCluster(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\tclusterId string,\n\tcursor *string,\n) (data_ *FindQueuesForClusterResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"FindQueuesForCluster\",\n\t\tQuery:  FindQueuesForCluster_Operation,\n\t\tVariables: &__FindQueuesForClusterInput{\n\t\t\tClusterId: clusterId,\n\t\t\tCursor:    cursor,\n\t\t},\n\t}\n\n\tdata_ = &FindQueuesForClusterResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The query executed by FindUserByEmail.\nconst FindUserByEmail_Operation = `\nquery FindUserByEmail ($organization: ID!, $email: String!) {\n\torganization(slug: $organization) {\n\t\tmembers(first: 1, email: $email) {\n\t\t\tedges {\n\t\t\t\tnode {\n\t\t\t\t\tuser {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n`\n\nfunc FindUserByEmail(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\torganization string,\n\temail string,\n) (data_ *FindUserByEmailResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"FindUserByEmail\",\n\t\tQuery:  FindUserByEmail_Operation,\n\t\tVariables: &__FindUserByEmailInput{\n\t\t\tOrganization: organization,\n\t\t\tEmail:        email,\n\t\t},\n\t}\n\n\tdata_ = &FindUserByEmailResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The query executed by GetClusterQueueAgent.\nconst GetClusterQueueAgent_Operation = `\nquery GetClusterQueueAgent ($orgSlug: ID!, $queueId: [ID!]) {\n\torganization(slug: $orgSlug) {\n\t\tagents(first: 10, clusterQueue: $queueId) {\n\t\t\tedges {\n\t\t\t\tnode {\n\t\t\t\t\tname\n\t\t\t\t\thostname\n\t\t\t\t\tversion\n\t\t\t\t\tid\n\t\t\t\t\tclusterQueue {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tuuid\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n`\n\nfunc GetClusterQueueAgent(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\torgSlug string,\n\tqueueId []string,\n) (data_ *GetClusterQueueAgentResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"GetClusterQueueAgent\",\n\t\tQuery:  GetClusterQueueAgent_Operation,\n\t\tVariables: &__GetClusterQueueAgentInput{\n\t\t\tOrgSlug: orgSlug,\n\t\t\tQueueId: queueId,\n\t\t},\n\t}\n\n\tdata_ = &GetClusterQueueAgentResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The query executed by GetClusterQueues.\nconst GetClusterQueues_Operation = `\nquery GetClusterQueues ($orgSlug: ID!, $clusterId: ID!) {\n\torganization(slug: $orgSlug) {\n\t\tcluster(id: $clusterId) {\n\t\t\tname\n\t\t\tdescription\n\t\t\tqueues(first: 10) {\n\t\t\t\tedges {\n\t\t\t\t\tnode {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tuuid\n\t\t\t\t\t\tkey\n\t\t\t\t\t\tdescription\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n`\n\nfunc GetClusterQueues(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\torgSlug string,\n\tclusterId string,\n) (data_ *GetClusterQueuesResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"GetClusterQueues\",\n\t\tQuery:  GetClusterQueues_Operation,\n\t\tVariables: &__GetClusterQueuesInput{\n\t\t\tOrgSlug:   orgSlug,\n\t\t\tClusterId: clusterId,\n\t\t},\n\t}\n\n\tdata_ = &GetClusterQueuesResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The query executed by GetOrganizationID.\nconst GetOrganizationID_Operation = `\nquery GetOrganizationID ($slug: ID!) {\n\torganization(slug: $slug) {\n\t\tid\n\t}\n}\n`\n\nfunc GetOrganizationID(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\tslug string,\n) (data_ *GetOrganizationIDResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"GetOrganizationID\",\n\t\tQuery:  GetOrganizationID_Operation,\n\t\tVariables: &__GetOrganizationIDInput{\n\t\t\tSlug: slug,\n\t\t},\n\t}\n\n\tdata_ = &GetOrganizationIDResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The mutation executed by InviteUser.\nconst InviteUser_Operation = `\nmutation InviteUser ($organization: ID!, $emails: [String!]!) {\n\torganizationInvitationCreate(input: {organizationID:$organization,emails:$emails}) {\n\t\tclientMutationId\n\t}\n}\n`\n\nfunc InviteUser(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\torganization string,\n\temails []string,\n) (data_ *InviteUserResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"InviteUser\",\n\t\tQuery:  InviteUser_Operation,\n\t\tVariables: &__InviteUserInput{\n\t\t\tOrganization: organization,\n\t\t\tEmails:       emails,\n\t\t},\n\t}\n\n\tdata_ = &InviteUserResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The query executed by ListJobsByAgentQueryRules.\nconst ListJobsByAgentQueryRules_Operation = `\nquery ListJobsByAgentQueryRules ($org: ID!, $agentQueryRules: [String!], $first: Int, $after: String) {\n\torganization(slug: $org) {\n\t\tjobs(first: $first, after: $after, agentQueryRules: $agentQueryRules) {\n\t\t\tedges {\n\t\t\t\tnode {\n\t\t\t\t\t__typename\n\t\t\t\t\t... on JobTypeCommand {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tuuid\n\t\t\t\t\t\tcommand\n\t\t\t\t\t\tstate\n\t\t\t\t\t\texitStatus\n\t\t\t\t\t\turl\n\t\t\t\t\t\tstartedAt\n\t\t\t\t\t\tfinishedAt\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tagent {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\thostname\n\t\t\t\t\t\t\tmetaData\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpageInfo {\n\t\t\t\tendCursor\n\t\t\t\thasNextPage\n\t\t\t}\n\t\t}\n\t}\n}\n`\n\nfunc ListJobsByAgentQueryRules(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\torg string,\n\tagentQueryRules []string,\n\tfirst *int,\n\tafter *string,\n) (data_ *ListJobsByAgentQueryRulesResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"ListJobsByAgentQueryRules\",\n\t\tQuery:  ListJobsByAgentQueryRules_Operation,\n\t\tVariables: &__ListJobsByAgentQueryRulesInput{\n\t\t\tOrg:             org,\n\t\t\tAgentQueryRules: agentQueryRules,\n\t\t\tFirst:           first,\n\t\t\tAfter:           after,\n\t\t},\n\t}\n\n\tdata_ = &ListJobsByAgentQueryRulesResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The query executed by ListJobsByQueue.\nconst ListJobsByQueue_Operation = `\nquery ListJobsByQueue ($org: ID!, $clusterQueue: [ID!], $first: Int, $after: String) {\n\torganization(slug: $org) {\n\t\tjobs(clusterQueue: $clusterQueue, first: $first, after: $after) {\n\t\t\tedges {\n\t\t\t\tnode {\n\t\t\t\t\t__typename\n\t\t\t\t\t... on JobTypeCommand {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tuuid\n\t\t\t\t\t\tcommand\n\t\t\t\t\t\tstate\n\t\t\t\t\t\texitStatus\n\t\t\t\t\t\turl\n\t\t\t\t\t\tstartedAt\n\t\t\t\t\t\tfinishedAt\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tcluster {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t\tclusterQueue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t}\n\t\t\t\t\t\tagent {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\thostname\n\t\t\t\t\t\t\tmetaData\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpageInfo {\n\t\t\t\tendCursor\n\t\t\t\thasNextPage\n\t\t\t}\n\t\t}\n\t}\n}\n`\n\nfunc ListJobsByQueue(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\torg string,\n\tclusterQueue []string,\n\tfirst *int,\n\tafter *string,\n) (data_ *ListJobsByQueueResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"ListJobsByQueue\",\n\t\tQuery:  ListJobsByQueue_Operation,\n\t\tVariables: &__ListJobsByQueueInput{\n\t\t\tOrg:          org,\n\t\t\tClusterQueue: clusterQueue,\n\t\t\tFirst:        first,\n\t\t\tAfter:        after,\n\t\t},\n\t}\n\n\tdata_ = &ListJobsByQueueResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The query executed by ListJobsByState.\nconst ListJobsByState_Operation = `\nquery ListJobsByState ($org: ID!, $state: [JobStates!], $first: Int, $after: String) {\n\torganization(slug: $org) {\n\t\tjobs(state: $state, first: $first, after: $after) {\n\t\t\tedges {\n\t\t\t\tnode {\n\t\t\t\t\t__typename\n\t\t\t\t\t... on JobTypeCommand {\n\t\t\t\t\t\tid\n\t\t\t\t\t\tuuid\n\t\t\t\t\t\tlabel\n\t\t\t\t\t\tcommand\n\t\t\t\t\t\tstate\n\t\t\t\t\t\texitStatus\n\t\t\t\t\t\turl\n\t\t\t\t\t\tstartedAt\n\t\t\t\t\t\tfinishedAt\n\t\t\t\t\t\tcreatedAt\n\t\t\t\t\t\tcluster {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t\tclusterQueue {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t}\n\t\t\t\t\t\tagent {\n\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\thostname\n\t\t\t\t\t\t\tmetaData\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tpageInfo {\n\t\t\t\tendCursor\n\t\t\t\thasNextPage\n\t\t\t}\n\t\t}\n\t}\n}\n`\n\nfunc ListJobsByState(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\torg string,\n\tstate []JobStates,\n\tfirst *int,\n\tafter *string,\n) (data_ *ListJobsByStateResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"ListJobsByState\",\n\t\tQuery:  ListJobsByState_Operation,\n\t\tVariables: &__ListJobsByStateInput{\n\t\t\tOrg:   org,\n\t\t\tState: state,\n\t\t\tFirst: first,\n\t\t\tAfter: after,\n\t\t},\n\t}\n\n\tdata_ = &ListJobsByStateResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The mutation executed by PipelineCreateWebhook.\nconst PipelineCreateWebhook_Operation = `\nmutation PipelineCreateWebhook ($id: ID!) {\n\tpipelineCreateWebhook(input: {id:$id}) {\n\t\tclientMutationId\n\t\tpipelineID\n\t}\n}\n`\n\nfunc PipelineCreateWebhook(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\tid string,\n) (data_ *PipelineCreateWebhookResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"PipelineCreateWebhook\",\n\t\tQuery:  PipelineCreateWebhook_Operation,\n\t\tVariables: &__PipelineCreateWebhookInput{\n\t\t\tId: id,\n\t\t},\n\t}\n\n\tdata_ = &PipelineCreateWebhookResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The mutation executed by RetryJob.\nconst RetryJob_Operation = `\nmutation RetryJob ($id: ID!) {\n\tjobTypeCommandRetry(input: {id:$id}) {\n\t\tjobTypeCommand {\n\t\t\tid\n\t\t\tstate\n\t\t\turl\n\t\t}\n\t}\n}\n`\n\nfunc RetryJob(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\tid string,\n) (data_ *RetryJobResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"RetryJob\",\n\t\tQuery:  RetryJob_Operation,\n\t\tVariables: &__RetryJobInput{\n\t\t\tId: id,\n\t\t},\n\t}\n\n\tdata_ = &RetryJobResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n\n// The mutation executed by UnblockJob.\nconst UnblockJob_Operation = `\nmutation UnblockJob ($id: ID!, $fields: JSON) {\n\tjobTypeBlockUnblock(input: {id:$id,fields:$fields}) {\n\t\tjobTypeBlock {\n\t\t\tid\n\t\t\tstate\n\t\t\tisUnblockable\n\t\t\tbuild {\n\t\t\t\turl\n\t\t\t}\n\t\t}\n\t}\n}\n`\n\nfunc UnblockJob(\n\tctx_ context.Context,\n\tclient_ graphql.Client,\n\tid string,\n\tfields *string,\n) (data_ *UnblockJobResponse, err_ error) {\n\treq_ := &graphql.Request{\n\t\tOpName: \"UnblockJob\",\n\t\tQuery:  UnblockJob_Operation,\n\t\tVariables: &__UnblockJobInput{\n\t\t\tId:     id,\n\t\t\tFields: fields,\n\t\t},\n\t}\n\n\tdata_ = &UnblockJobResponse{}\n\tresp_ := &graphql.Response{Data: data_}\n\n\terr_ = client_.MakeRequest(\n\t\tctx_,\n\t\treq_,\n\t\tresp_,\n\t)\n\n\treturn data_, err_\n}\n"
  },
  {
    "path": "internal/http/README.md",
    "content": "# HTTP Client Package\n\nThis package provides a common HTTP client with standardized headers and error handling for Buildkite API requests.\n\n## Features\n\n- Standardized authorization header handling\n- Common error handling for API responses\n- Support for different HTTP methods (GET, POST, PUT, DELETE)\n- JSON request and response handling\n- Configurable base URL and user agent\n\n## Usage\n\n### Creating a client\n\n```go\nimport (\n    \"github.com/buildkite/cli/v3/internal/http\"\n)\n\n// Basic client with token\nclient := http.NewClient(\"your-api-token\")\n\n// Client with custom base URL\nclient := http.NewClient(\n    \"your-api-token\",\n    http.WithBaseURL(\"https://api.example.com\"),\n)\n\n// Client with custom user agent\nclient := http.NewClient(\n    \"your-api-token\",\n    http.WithUserAgent(\"my-app/1.0\"),\n)\n\n// Client with custom HTTP client\nclient := http.NewClient(\n    \"your-api-token\",\n    http.WithHTTPClient(customHTTPClient),\n)\n```\n\n### Making requests\n\n```go\n// GET request\nvar response SomeResponseType\nerr := client.Get(ctx, \"/endpoint\", &response)\n\n// POST request with body\nrequestBody := map[string]string{\"key\": \"value\"}\nvar response SomeResponseType\nerr := client.Post(ctx, \"/endpoint\", requestBody, &response)\n\n// PUT request\nerr := client.Put(ctx, \"/endpoint\", requestBody, &response)\n\n// DELETE request\nerr := client.Delete(ctx, \"/endpoint\", &response)\n\n// Custom method\nerr := client.Do(ctx, \"PATCH\", \"/endpoint\", requestBody, &response)\n```\n\n### Error handling\n\n```go\nerr := client.Get(ctx, \"/endpoint\", &response)\nif err != nil {\n    // Check if it's an HTTP error\n    if httpErr, ok := err.(*http.ErrorResponse); ok {\n        fmt.Printf(\"HTTP error: %d %s\\n\", httpErr.StatusCode, httpErr.Status)\n        fmt.Printf(\"Response body: %s\\n\", httpErr.Body)\n    } else {\n        fmt.Printf(\"Other error: %v\\n\", err)\n    }\n}\n```\n"
  },
  {
    "path": "internal/http/client.go",
    "content": "package http\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n)\n\n// ErrorResponse represents an error response from the API\ntype ErrorResponse struct {\n\tStatusCode int\n\tStatus     string\n\tURL        string\n\tBody       []byte\n\tHeaders    http.Header\n}\n\n// Error implements the error interface\nfunc (e *ErrorResponse) Error() string {\n\tmsg := fmt.Sprintf(\"HTTP request failed: %d %s (%s)\", e.StatusCode, e.Status, e.URL)\n\tif len(e.Body) > 0 {\n\t\t// Truncate body if it's very long for the error message\n\t\tbodyStr := string(e.Body)\n\t\tif len(bodyStr) > 200 {\n\t\t\tbodyStr = bodyStr[:200] + \"...\"\n\t\t}\n\t\tmsg += fmt.Sprintf(\": %s\", bodyStr)\n\t}\n\treturn msg\n}\n\n// Client is an HTTP client that handles common operations for Buildkite API requests\ntype Client struct {\n\tbaseURL   string\n\ttoken     string\n\tuserAgent string\n\tclient    *http.Client\n}\n\n// ClientOption is a function that modifies a Client\ntype ClientOption func(*Client)\n\n// WithBaseURL sets the base URL for API requests\nfunc WithBaseURL(baseURL string) ClientOption {\n\treturn func(c *Client) {\n\t\tc.baseURL = baseURL\n\t}\n}\n\n// WithUserAgent sets the User-Agent header for requests\nfunc WithUserAgent(userAgent string) ClientOption {\n\treturn func(c *Client) {\n\t\tc.userAgent = userAgent\n\t}\n}\n\n// WithHTTPClient sets the underlying HTTP client\nfunc WithHTTPClient(client *http.Client) ClientOption {\n\treturn func(c *Client) {\n\t\tc.client = client\n\t}\n}\n\n// NewClient creates a new HTTP client with the given token and options\nfunc NewClient(token string, opts ...ClientOption) *Client {\n\tc := &Client{\n\t\tbaseURL:   \"https://api.buildkite.com\",\n\t\ttoken:     token,\n\t\tuserAgent: \"buildkite-cli\",\n\t\tclient:    http.DefaultClient,\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(c)\n\t}\n\n\treturn c\n}\n\n// Get performs a GET request to the specified endpoint\nfunc (c *Client) Get(ctx context.Context, endpoint string, v interface{}) error {\n\treturn c.Do(ctx, http.MethodGet, endpoint, nil, v)\n}\n\n// Post performs a POST request to the specified endpoint with the given body\nfunc (c *Client) Post(ctx context.Context, endpoint string, body interface{}, v interface{}) error {\n\treturn c.Do(ctx, http.MethodPost, endpoint, body, v)\n}\n\n// Put performs a PUT request to the specified endpoint with the given body\nfunc (c *Client) Put(ctx context.Context, endpoint string, body interface{}, v interface{}) error {\n\treturn c.Do(ctx, http.MethodPut, endpoint, body, v)\n}\n\n// Delete performs a DELETE request to the specified endpoint\nfunc (c *Client) Delete(ctx context.Context, endpoint string, v interface{}) error {\n\treturn c.Do(ctx, http.MethodDelete, endpoint, nil, v)\n}\n\n// IsNotFound returns true if the error is a 404 Not Found\nfunc (e *ErrorResponse) IsNotFound() bool {\n\treturn e.StatusCode == http.StatusNotFound\n}\n\n// IsUnauthorized returns true if the error is a 401 Unauthorized\nfunc (e *ErrorResponse) IsUnauthorized() bool {\n\treturn e.StatusCode == http.StatusUnauthorized\n}\n\n// IsForbidden returns true if the error is a 403 Forbidden\nfunc (e *ErrorResponse) IsForbidden() bool {\n\treturn e.StatusCode == http.StatusForbidden\n}\n\n// IsBadRequest returns true if the error is a 400 Bad Request\nfunc (e *ErrorResponse) IsBadRequest() bool {\n\treturn e.StatusCode == http.StatusBadRequest\n}\n\n// IsServerError returns true if the error is a 5xx Server Error\nfunc (e *ErrorResponse) IsServerError() bool {\n\treturn e.StatusCode >= 500\n}\n\n// IsTooManyRequests returns true if the error is a 429 Too Many Requests\nfunc (e *ErrorResponse) IsTooManyRequests() bool {\n\treturn e.StatusCode == http.StatusTooManyRequests\n}\n\n// Do performs an HTTP request with the given method, endpoint, and body.\nfunc (c *Client) Do(ctx context.Context, method, endpoint string, body interface{}, v interface{}) error {\n\t// Ensure endpoint starts with \"/\"\n\tif !strings.HasPrefix(endpoint, \"/\") {\n\t\tendpoint = \"/\" + endpoint\n\t}\n\n\t// Parse the endpoint to properly handle path, query string, and fragments\n\tparsedEndpoint, err := url.Parse(endpoint)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse endpoint: %w\", err)\n\t}\n\n\t// Create the request URL using only the path portion\n\treqURL, err := url.JoinPath(c.baseURL, parsedEndpoint.Path)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create request URL: %w\", err)\n\t}\n\n\t// Reattach query string if present (properly encoded)\n\tif parsedEndpoint.RawQuery != \"\" {\n\t\treqURL += \"?\" + parsedEndpoint.RawQuery\n\t}\n\n\tvar bodyBytes []byte\n\tif body != nil {\n\t\t// We need to nest this in a branch because otherwise\n\t\t// `json.Marshal(nil)` produces `null` instead of `nil`.\n\t\tbodyBytes, err = json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to marshal request body: %w\", err)\n\t\t}\n\t}\n\n\trespBody, err := c.send(ctx, method, reqURL, bodyBytes)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif v != nil && len(respBody) > 0 {\n\t\tif err := json.Unmarshal(respBody, v); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) send(ctx context.Context, method, reqURL string, body []byte) ([]byte, error) {\n\t// Create the request\n\treq, err := http.NewRequestWithContext(ctx, method, reqURL, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\t// Set common headers\n\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", c.token))\n\treq.Header.Set(\"User-Agent\", c.userAgent)\n\tif body != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t}\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\t// Execute the request\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\t// Read response body\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read response body: %w\", err)\n\t}\n\n\t// Check for error status\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, &ErrorResponse{\n\t\t\tStatusCode: resp.StatusCode,\n\t\t\tStatus:     resp.Status,\n\t\t\tURL:        reqURL,\n\t\t\tBody:       respBody,\n\t\t\tHeaders:    resp.Header,\n\t\t}\n\t}\n\n\treturn respBody, nil\n}\n"
  },
  {
    "path": "internal/http/client_test.go",
    "content": "package http\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\ntype testResponse struct {\n\tMessage string `json:\"message\"`\n}\n\nfunc TestClient(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"makes request with authorization header\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar receivedToken string\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\treceivedToken = r.Header.Get(\"Authorization\")\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tjson.NewEncoder(w).Encode(testResponse{Message: \"success\"})\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tclient := NewClient(\"test-token\", WithBaseURL(server.URL))\n\n\t\tvar resp testResponse\n\t\terr := client.Get(context.Background(), \"/test\", &resp)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\texpectedToken := \"Bearer test-token\"\n\t\tif receivedToken != expectedToken {\n\t\t\tt.Errorf(\"expected Authorization header %q, got %q\", expectedToken, receivedToken)\n\t\t}\n\n\t\tif resp.Message != \"success\" {\n\t\t\tt.Errorf(\"expected response message %q, got %q\", \"success\", resp.Message)\n\t\t}\n\t})\n\n\tt.Run(\"handles JSON response\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(testResponse{Message: \"test message\"})\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tclient := NewClient(\"test-token\", WithBaseURL(server.URL))\n\n\t\tvar resp testResponse\n\t\terr := client.Get(context.Background(), \"/test\", &resp)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tif resp.Message != \"test message\" {\n\t\t\tt.Errorf(\"expected message %q, got %q\", \"test message\", resp.Message)\n\t\t}\n\t})\n\n\tt.Run(\"handles error response\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tjson.NewEncoder(w).Encode(map[string]string{\"error\": \"bad request\"})\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tclient := NewClient(\"test-token\", WithBaseURL(server.URL))\n\n\t\tvar resp testResponse\n\t\terr := client.Get(context.Background(), \"/test\", &resp)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"expected error, got nil\")\n\t\t}\n\n\t\t// Error should contain status code and possibly the error message\n\t\tif errStr := err.Error(); errStr == \"\" {\n\t\t\tt.Error(\"expected non-empty error message\")\n\t\t}\n\t})\n\n\tt.Run(\"adds user agent header\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar receivedUA string\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\treceivedUA = r.Header.Get(\"User-Agent\")\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tjson.NewEncoder(w).Encode(testResponse{Message: \"success\"})\n\t\t}))\n\t\tdefer server.Close()\n\n\t\texpectedUA := \"test-user-agent\"\n\t\tclient := NewClient(\"test-token\", WithBaseURL(server.URL), WithUserAgent(expectedUA))\n\n\t\tvar resp testResponse\n\t\terr := client.Get(context.Background(), \"/test\", &resp)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tif receivedUA != expectedUA {\n\t\t\tt.Errorf(\"expected User-Agent header %q, got %q\", expectedUA, receivedUA)\n\t\t}\n\t})\n\n\tt.Run(\"handles POST request with body\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar receivedBody []byte\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tvar err error\n\t\t\treceivedBody, err = io.ReadAll(r.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to read request body: %v\", err)\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tjson.NewEncoder(w).Encode(testResponse{Message: \"success\"})\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tclient := NewClient(\"test-token\", WithBaseURL(server.URL))\n\n\t\trequestBody := map[string]string{\"test\": \"data\"}\n\t\tvar resp testResponse\n\t\terr := client.Post(context.Background(), \"/test\", requestBody, &resp)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\t// Check that the body was correctly serialized\n\t\tvar parsed map[string]string\n\t\tif err := json.Unmarshal(receivedBody, &parsed); err != nil {\n\t\t\tt.Fatalf(\"failed to parse received body: %v\", err)\n\t\t}\n\t\tif parsed[\"test\"] != \"data\" {\n\t\t\tt.Errorf(\"expected body to contain %q, got %q\", \"data\", parsed[\"test\"])\n\t\t}\n\t})\n\n\tt.Run(\"preserves query parameters in endpoint\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar receivedQuery string\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\treceivedQuery = r.URL.RawQuery\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tjson.NewEncoder(w).Encode(testResponse{Message: \"success\"})\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tclient := NewClient(\"test-token\", WithBaseURL(server.URL))\n\n\t\tvar resp testResponse\n\t\terr := client.Get(context.Background(), \"/builds?branch=main&state=passed\", &resp)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\texpectedQuery := \"branch=main&state=passed\"\n\t\tif receivedQuery != expectedQuery {\n\t\t\tt.Errorf(\"expected query string %q, got %q\", expectedQuery, receivedQuery)\n\t\t}\n\t})\n\n\tt.Run(\"handles encoded query parameters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar receivedQuery string\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\treceivedQuery = r.URL.RawQuery\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tjson.NewEncoder(w).Encode(testResponse{Message: \"success\"})\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tclient := NewClient(\"test-token\", WithBaseURL(server.URL))\n\n\t\tvar resp testResponse\n\t\terr := client.Get(context.Background(), \"/builds?branch=feature%2Ftest%20name\", &resp)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\texpectedQuery := \"branch=feature%2Ftest%20name\"\n\t\tif receivedQuery != expectedQuery {\n\t\t\tt.Errorf(\"expected query string %q, got %q\", expectedQuery, receivedQuery)\n\t\t}\n\t})\n\n\tt.Run(\"strips fragments from endpoint\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar receivedPath string\n\t\tvar receivedQuery string\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\treceivedPath = r.URL.Path\n\t\t\treceivedQuery = r.URL.RawQuery\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tjson.NewEncoder(w).Encode(testResponse{Message: \"success\"})\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tclient := NewClient(\"test-token\", WithBaseURL(server.URL))\n\n\t\tvar resp testResponse\n\t\terr := client.Get(context.Background(), \"/builds?branch=main#foo\", &resp)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\tif receivedPath != \"/builds\" {\n\t\t\tt.Errorf(\"expected path %q, got %q\", \"/builds\", receivedPath)\n\t\t}\n\t\texpectedQuery := \"branch=main\"\n\t\tif receivedQuery != expectedQuery {\n\t\t\tt.Errorf(\"expected query string %q, got %q\", expectedQuery, receivedQuery)\n\t\t}\n\t})\n\n\tt.Run(\"handles endpoint without query parameters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar receivedPath string\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\treceivedPath = r.URL.Path\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tjson.NewEncoder(w).Encode(testResponse{Message: \"success\"})\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tclient := NewClient(\"test-token\", WithBaseURL(server.URL))\n\n\t\tvar resp testResponse\n\t\terr := client.Get(context.Background(), \"/pipelines/test/builds\", &resp)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\n\t\texpectedPath := \"/pipelines/test/builds\"\n\t\tif receivedPath != expectedPath {\n\t\t\tt.Errorf(\"expected path %q, got %q\", expectedPath, receivedPath)\n\t\t}\n\t})\n}\n\nfunc TestErrorResponse(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"formats status code errors\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\terr := &ErrorResponse{\n\t\t\tStatusCode: 404,\n\t\t\tStatus:     \"Not Found\",\n\t\t\tURL:        \"https://example.com/resource\",\n\t\t}\n\n\t\texpected := \"HTTP request failed: 404 Not Found (https://example.com/resource)\"\n\t\tif err.Error() != expected {\n\t\t\tt.Errorf(\"expected error message %q, got %q\", expected, err.Error())\n\t\t}\n\t})\n\n\tt.Run(\"includes body in error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\terr := &ErrorResponse{\n\t\t\tStatusCode: 400,\n\t\t\tStatus:     \"Bad Request\",\n\t\t\tURL:        \"https://example.com/resource\",\n\t\t\tBody:       []byte(`{\"error\":\"Invalid input\"}`),\n\t\t}\n\n\t\texpected := \"HTTP request failed: 400 Bad Request (https://example.com/resource): {\\\"error\\\":\\\"Invalid input\\\"}\"\n\t\tif err.Error() != expected {\n\t\t\tt.Errorf(\"expected error message %q, got %q\", expected, err.Error())\n\t\t}\n\t})\n\n\tt.Run(\"IsTooManyRequests returns true for 429\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\terr := &ErrorResponse{StatusCode: 429}\n\t\tif !err.IsTooManyRequests() {\n\t\t\tt.Error(\"expected IsTooManyRequests to return true for 429\")\n\t\t}\n\n\t\terr = &ErrorResponse{StatusCode: 500}\n\t\tif err.IsTooManyRequests() {\n\t\t\tt.Error(\"expected IsTooManyRequests to return false for 500\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/http/ratelimit.go",
    "content": "package http\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n)\n\nconst (\n\t// DefaultMaxRateLimitRetries is the default number of times to retry a\n\t// rate-limited request.\n\tDefaultMaxRateLimitRetries = 3\n\n\t// defaultFallbackDelay is used when the server returns 429 but the\n\t// RateLimit-Reset header is missing or unparseable.\n\tdefaultFallbackDelay = 10 * time.Second\n)\n\n// OnRateLimitFunc is called before sleeping for a rate-limit backoff.\n// attempt is zero-indexed; delay is how long the transport will sleep.\ntype OnRateLimitFunc func(attempt int, delay time.Duration)\n\n// RateLimitTransport wraps an http.RoundTripper and automatically retries\n// requests that receive an HTTP 429 response, sleeping for the duration\n// indicated by the RateLimit-Reset header.\ntype RateLimitTransport struct {\n\t// Transport is the underlying RoundTripper. If nil, http.DefaultTransport\n\t// is used.\n\tTransport http.RoundTripper\n\n\t// MaxRetries is the maximum number of retry attempts on 429. Zero means\n\t// no retries; negative values are treated as zero.\n\tMaxRetries int\n\n\t// MaxRetryDelay caps the sleep duration for any single retry. Zero means\n\t// no cap is applied.\n\tMaxRetryDelay time.Duration\n\n\t// OnRateLimit is an optional callback invoked before each backoff sleep.\n\tOnRateLimit OnRateLimitFunc\n}\n\n// NewRateLimitTransport returns a RateLimitTransport wrapping the given\n// transport with sensible defaults.\nfunc NewRateLimitTransport(transport http.RoundTripper) *RateLimitTransport {\n\tif transport == nil {\n\t\ttransport = http.DefaultTransport\n\t}\n\treturn &RateLimitTransport{\n\t\tTransport:  transport,\n\t\tMaxRetries: DefaultMaxRateLimitRetries,\n\t}\n}\n\n// RoundTrip implements http.RoundTripper. On a 429 response it reads the\n// RateLimit-Reset header (seconds until the rate-limit window resets) and\n// sleeps for that duration before retrying, up to MaxRetries times.\nfunc (t *RateLimitTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\ttransport := t.Transport\n\tif transport == nil {\n\t\ttransport = http.DefaultTransport\n\t}\n\n\tfor attempt := 0; ; attempt++ {\n\t\t// Reset the request body for retries.\n\t\tif attempt > 0 && req.GetBody != nil {\n\t\t\tbody, err := req.GetBody()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treq.Body = body\n\t\t}\n\n\t\tresp, err := transport.RoundTrip(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif resp.StatusCode != http.StatusTooManyRequests || attempt >= t.MaxRetries {\n\t\t\treturn resp, nil\n\t\t}\n\n\t\tdelay, ok := parseRateLimitReset(resp)\n\t\tif !ok {\n\t\t\tdelay = defaultFallbackDelay\n\t\t}\n\t\tif t.MaxRetryDelay > 0 && delay > t.MaxRetryDelay {\n\t\t\tdelay = t.MaxRetryDelay\n\t\t}\n\n\t\t// Drain and close the 429 response body before retrying.\n\t\t_, _ = io.Copy(io.Discard, resp.Body)\n\t\tresp.Body.Close()\n\n\t\tif t.OnRateLimit != nil {\n\t\t\tt.OnRateLimit(attempt, delay)\n\t\t}\n\n\t\t// Sleep for the backoff duration, but honour context cancellation.\n\t\ttimer := time.NewTimer(delay)\n\t\tselect {\n\t\tcase <-req.Context().Done():\n\t\t\ttimer.Stop()\n\t\t\treturn nil, req.Context().Err()\n\t\tcase <-timer.C:\n\t\t}\n\t}\n}\n\n// parseRateLimitReset reads the RateLimit-Reset header and returns the\n// duration to wait plus a boolean indicating whether the value was valid.\nfunc parseRateLimitReset(resp *http.Response) (time.Duration, bool) {\n\ts := resp.Header.Get(\"RateLimit-Reset\")\n\tif s == \"\" {\n\t\treturn 0, false\n\t}\n\tseconds, err := strconv.Atoi(s)\n\tif err != nil || seconds < 0 {\n\t\treturn 0, false\n\t}\n\treturn time.Duration(seconds) * time.Second, true\n}\n"
  },
  {
    "path": "internal/http/ratelimit_test.go",
    "content": "package http\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestRateLimitTransport(t *testing.T) {\n\tt.Run(\"passes through non-429 responses\", func(t *testing.T) {\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tw.Write([]byte(\"ok\"))\n\t\t}))\n\t\tdefer s.Close()\n\n\t\trt := NewRateLimitTransport(http.DefaultTransport)\n\t\treq, _ := http.NewRequest(\"GET\", s.URL, nil)\n\t\tresp, err := rt.RoundTrip(req)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Errorf(\"expected 200, got %d\", resp.StatusCode)\n\t\t}\n\t})\n\n\tt.Run(\"retries on 429 and succeeds\", func(t *testing.T) {\n\t\tvar attempts atomic.Int32\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tn := attempts.Add(1)\n\t\t\tif n <= 2 {\n\t\t\t\tw.Header().Set(\"RateLimit-Reset\", \"1\")\n\t\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t\t\tw.Write([]byte(\"rate limited\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tw.Write([]byte(\"ok\"))\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tvar callbackCalls int\n\t\trt := NewRateLimitTransport(http.DefaultTransport)\n\t\trt.MaxRetries = 3\n\t\trt.OnRateLimit = func(attempt int, delay time.Duration) {\n\t\t\tcallbackCalls++\n\t\t}\n\n\t\treq, _ := http.NewRequest(\"GET\", s.URL, nil)\n\t\tresp, err := rt.RoundTrip(req)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Errorf(\"expected 200 after retries, got %d\", resp.StatusCode)\n\t\t}\n\t\tif got := attempts.Load(); got != 3 {\n\t\t\tt.Errorf(\"expected 3 total attempts, got %d\", got)\n\t\t}\n\t\tif callbackCalls != 2 {\n\t\t\tt.Errorf(\"expected 2 callback calls, got %d\", callbackCalls)\n\t\t}\n\t})\n\n\tt.Run(\"returns 429 after exhausting retries\", func(t *testing.T) {\n\t\tvar attempts atomic.Int32\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tattempts.Add(1)\n\t\t\tw.Header().Set(\"RateLimit-Reset\", \"1\")\n\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\trt := NewRateLimitTransport(http.DefaultTransport)\n\t\trt.MaxRetries = 2\n\n\t\treq, _ := http.NewRequest(\"GET\", s.URL, nil)\n\t\tresp, err := rt.RoundTrip(req)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tif resp.StatusCode != http.StatusTooManyRequests {\n\t\t\tt.Errorf(\"expected 429 after exhausting retries, got %d\", resp.StatusCode)\n\t\t}\n\t\t// 1 initial + 2 retries = 3 total\n\t\tif got := attempts.Load(); got != 3 {\n\t\t\tt.Errorf(\"expected 3 total attempts, got %d\", got)\n\t\t}\n\t})\n\n\tt.Run(\"respects context cancellation during backoff\", func(t *testing.T) {\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(\"RateLimit-Reset\", \"60\")\n\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\trt := NewRateLimitTransport(http.DefaultTransport)\n\t\trt.MaxRetries = 3\n\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t// Cancel shortly after the first 429 is received.\n\t\trt.OnRateLimit = func(attempt int, delay time.Duration) {\n\t\t\tgo func() {\n\t\t\t\ttime.Sleep(10 * time.Millisecond)\n\t\t\t\tcancel()\n\t\t\t}()\n\t\t}\n\n\t\treq, _ := http.NewRequestWithContext(ctx, \"GET\", s.URL, nil)\n\t\t_, err := rt.RoundTrip(req)\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected error from cancelled context, got nil\")\n\t\t}\n\t\tif !strings.Contains(err.Error(), \"context canceled\") {\n\t\t\tt.Errorf(\"expected context canceled error, got: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"uses fallback delay when header missing\", func(t *testing.T) {\n\t\tvar attempts atomic.Int32\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tn := attempts.Add(1)\n\t\t\tif n == 1 {\n\t\t\t\t// No RateLimit-Reset header\n\t\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tvar gotDelay time.Duration\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tdefer cancel()\n\n\t\trt := NewRateLimitTransport(http.DefaultTransport)\n\t\trt.MaxRetries = 1\n\t\t// Cancel quickly to avoid waiting the full fallback delay.\n\t\trt.OnRateLimit = func(attempt int, delay time.Duration) {\n\t\t\tgotDelay = delay\n\t\t\tcancel()\n\t\t}\n\n\t\treq, _ := http.NewRequestWithContext(ctx, \"GET\", s.URL, nil)\n\t\trt.RoundTrip(req)\n\n\t\tif gotDelay != defaultFallbackDelay {\n\t\t\tt.Errorf(\"expected fallback delay %v, got %v\", defaultFallbackDelay, gotDelay)\n\t\t}\n\t})\n\n\tt.Run(\"uses zero delay when header is zero\", func(t *testing.T) {\n\t\tvar attempts atomic.Int32\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tn := attempts.Add(1)\n\t\t\tif n == 1 {\n\t\t\t\tw.Header().Set(\"RateLimit-Reset\", \"0\")\n\t\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\tvar gotDelay time.Duration\n\t\trt := NewRateLimitTransport(http.DefaultTransport)\n\t\trt.MaxRetries = 1\n\t\trt.OnRateLimit = func(attempt int, delay time.Duration) {\n\t\t\tgotDelay = delay\n\t\t}\n\n\t\treq, _ := http.NewRequest(\"GET\", s.URL, nil)\n\t\tresp, err := rt.RoundTrip(req)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tif gotDelay != 0 {\n\t\t\tt.Errorf(\"expected zero delay, got %v\", gotDelay)\n\t\t}\n\t})\n\n\tt.Run(\"caps delay at MaxRetryDelay\", func(t *testing.T) {\n\t\tvar attempts atomic.Int32\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tn := attempts.Add(1)\n\t\t\tif n == 1 {\n\t\t\t\tw.Header().Set(\"RateLimit-Reset\", \"3600\")\n\t\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\trt := NewRateLimitTransport(http.DefaultTransport)\n\t\trt.MaxRetries = 1\n\t\trt.MaxRetryDelay = 10 * time.Millisecond\n\n\t\treq, _ := http.NewRequest(\"GET\", s.URL, nil)\n\t\tstart := time.Now()\n\t\tresp, err := rt.RoundTrip(req)\n\t\telapsed := time.Since(start)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Errorf(\"expected 200, got %d\", resp.StatusCode)\n\t\t}\n\t\tif elapsed > 1*time.Second {\n\t\t\tt.Errorf(\"expected delay to be capped, but took %v\", elapsed)\n\t\t}\n\t})\n\n\tt.Run(\"replays request body on retry\", func(t *testing.T) {\n\t\tvar attempts atomic.Int32\n\t\tvar bodies []string\n\t\ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tb, _ := io.ReadAll(r.Body)\n\t\t\tbodies = append(bodies, string(b))\n\t\t\tn := attempts.Add(1)\n\t\t\tif n == 1 {\n\t\t\t\tw.Header().Set(\"RateLimit-Reset\", \"1\")\n\t\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t}))\n\t\tdefer s.Close()\n\n\t\trt := NewRateLimitTransport(http.DefaultTransport)\n\t\trt.MaxRetries = 1\n\n\t\tbody := `{\"key\":\"value\"}`\n\t\treq, _ := http.NewRequest(\"POST\", s.URL, strings.NewReader(body))\n\t\tresp, err := rt.RoundTrip(req)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tif len(bodies) != 2 {\n\t\t\tt.Fatalf(\"expected 2 requests, got %d\", len(bodies))\n\t\t}\n\t\tfor i, got := range bodies {\n\t\t\tif got != body {\n\t\t\t\tt.Errorf(\"attempt %d: body = %q, want %q\", i, got, body)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestParseRateLimitReset(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\theader   string\n\t\texpected time.Duration\n\t\tok       bool\n\t}{\n\t\t{\"valid seconds\", \"30\", 30 * time.Second, true},\n\t\t{\"one second\", \"1\", 1 * time.Second, true},\n\t\t{\"empty\", \"\", 0, false},\n\t\t{\"negative\", \"-1\", 0, false},\n\t\t{\"zero means retry now\", \"0\", 0, true},\n\t\t{\"non-numeric\", \"abc\", 0, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresp := &http.Response{Header: http.Header{}}\n\t\t\tif tt.header != \"\" {\n\t\t\t\tresp.Header.Set(\"RateLimit-Reset\", tt.header)\n\t\t\t}\n\t\t\tgot, ok := parseRateLimitReset(resp)\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"parseRateLimitReset(%q) = %v, want %v\", tt.header, got, tt.expected)\n\t\t\t}\n\t\t\tif ok != tt.ok {\n\t\t\t\tt.Errorf(\"parseRateLimitReset(%q) ok = %v, want %v\", tt.header, ok, tt.ok)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/http/refresh_transport.go",
    "content": "package http\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/buildkite/cli/v3/pkg/keyring\"\n\t\"github.com/buildkite/cli/v3/pkg/oauth\"\n)\n\n// TokenSource provides thread-safe access to the current access token.\n// It is shared between auth-injection points (REST, GraphQL) and\n// RefreshTransport so that a refreshed token is immediately visible\n// to all subsequent requests.\ntype TokenSource struct {\n\tmu    sync.RWMutex\n\ttoken string\n}\n\n// NewTokenSource creates a TokenSource initialised with the given token.\nfunc NewTokenSource(token string) *TokenSource {\n\treturn &TokenSource{token: token}\n}\n\n// Token returns the current access token.\nfunc (ts *TokenSource) Token() string {\n\tts.mu.RLock()\n\tdefer ts.mu.RUnlock()\n\treturn ts.token\n}\n\n// SetToken updates the current access token.\nfunc (ts *TokenSource) SetToken(token string) {\n\tts.mu.Lock()\n\tdefer ts.mu.Unlock()\n\tts.token = token\n}\n\n// AuthTransport injects the Authorization header from a TokenSource\n// on every outgoing request. It should wrap the base transport so that\n// RefreshTransport (which sits outside it) can override the header on\n// retries.\ntype AuthTransport struct {\n\tBase        http.RoundTripper\n\tTokenSource *TokenSource\n\tUserAgent   string\n}\n\nfunc (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\ttoken := t.TokenSource.Token()\n\tif token != \"\" {\n\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", token))\n\t}\n\tif t.UserAgent != \"\" {\n\t\treq.Header.Set(\"User-Agent\", t.UserAgent)\n\t}\n\tbase := t.Base\n\tif base == nil {\n\t\tbase = http.DefaultTransport\n\t}\n\treturn base.RoundTrip(req)\n}\n\n// RefreshTransport wraps an http.RoundTripper to automatically refresh\n// expired OAuth access tokens using a stored refresh token.\n//\n// On a 401 response it:\n//  1. Acquires a mutex to serialise concurrent refreshes.\n//  2. Checks whether the token has already been refreshed by another\n//     goroutine (compare-after-lock).\n//  3. If not, exchanges the refresh token for new tokens.\n//  4. Persists the new tokens and updates the shared TokenSource.\n//  5. Retries the original request with the new token.\ntype RefreshTransport struct {\n\tBase        http.RoundTripper\n\tOrg         string\n\tKeyring     *keyring.Keyring\n\tTokenSource *TokenSource\n\n\tmu sync.Mutex\n}\n\nfunc (t *RefreshTransport) base() http.RoundTripper {\n\tif t.Base != nil {\n\t\treturn t.Base\n\t}\n\treturn http.DefaultTransport\n}\n\nfunc (t *RefreshTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\t// Buffer the request body so it can be replayed on retry.\n\t// http.NewRequest sets GetBody for standard body types, but\n\t// custom readers (e.g. from GraphQL clients) may not.\n\tbufferRequestBody(req)\n\n\tresp, err := t.base().RoundTrip(req)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\tif resp.StatusCode != http.StatusUnauthorized {\n\t\treturn resp, nil\n\t}\n\n\t// Only attempt refresh if we have a refresh token\n\trefreshToken, rtErr := t.Keyring.GetRefreshToken(t.Org)\n\tif rtErr != nil || refreshToken == \"\" {\n\t\treturn resp, nil\n\t}\n\n\t// Extract the token that was used for the failed request so we can\n\t// detect whether another goroutine already refreshed it.\n\tfailedToken := extractBearerToken(req.Header.Get(\"Authorization\"))\n\n\t// Attempt token refresh (serialised to prevent concurrent refreshes)\n\tt.mu.Lock()\n\tnewToken, refreshErr := t.doRefresh(req.Context(), failedToken)\n\tt.mu.Unlock()\n\n\tif refreshErr != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Warning: token refresh failed: %v\\n\", refreshErr)\n\t\treturn resp, nil\n\t}\n\n\t// Drain and close the original 401 response body\n\t_, _ = io.Copy(io.Discard, resp.Body)\n\t_ = resp.Body.Close()\n\n\t// Clone the request with the new token and retry\n\tretryReq := req.Clone(req.Context())\n\tretryReq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", newToken))\n\n\t// Re-create the body for the retry\n\tif req.GetBody != nil {\n\t\tbody, err := req.GetBody()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get request body for retry: %w\", err)\n\t\t}\n\t\tretryReq.Body = body\n\t}\n\n\treturn t.base().RoundTrip(retryReq)\n}\n\nfunc (t *RefreshTransport) doRefresh(ctx context.Context, failedToken string) (string, error) {\n\t// Compare-after-lock: if the current token differs from the one that\n\t// failed, another goroutine already refreshed successfully. Skip the\n\t// refresh and use the new token.\n\tcurrentToken := t.TokenSource.Token()\n\tif currentToken != \"\" && currentToken != failedToken {\n\t\treturn currentToken, nil\n\t}\n\n\t// Re-read the refresh token under the lock — it may have been rotated\n\t// by a concurrent refresh.\n\trefreshToken, err := t.Keyring.GetRefreshToken(t.Org)\n\tif err != nil || refreshToken == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no refresh token available\")\n\t}\n\n\ttokenResp, err := oauth.RefreshAccessToken(ctx, \"\", \"\", refreshToken)\n\tif err != nil {\n\t\t// Only clear the stored refresh token on explicit grant errors\n\t\t// (invalid/expired/revoked). Transient failures (network, 5xx)\n\t\t// should not destroy the user's session.\n\t\tif isTerminalRefreshError(err) {\n\t\t\t_ = t.Keyring.DeleteRefreshToken(t.Org)\n\t\t}\n\t\treturn \"\", err\n\t}\n\n\t// Persist the new access token\n\tif err := t.Keyring.Set(t.Org, tokenResp.AccessToken); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to store refreshed access token: %w\", err)\n\t}\n\tt.TokenSource.SetToken(tokenResp.AccessToken)\n\n\t// Rotate the refresh token if a new one was issued\n\tif tokenResp.RefreshToken != \"\" {\n\t\tif err := t.Keyring.SetRefreshToken(t.Org, tokenResp.RefreshToken); err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Warning: failed to store rotated refresh token: %v\\n\", err)\n\t\t}\n\t}\n\n\treturn tokenResp.AccessToken, nil\n}\n\n// isTerminalRefreshError returns true for OAuth errors that indicate the\n// refresh token is permanently invalid and should be cleared.\nfunc isTerminalRefreshError(err error) bool {\n\tmsg := err.Error()\n\treturn strings.Contains(msg, \"invalid_grant\") ||\n\t\tstrings.Contains(msg, \"unauthorized_client\") ||\n\t\tstrings.Contains(msg, \"invalid_client\")\n}\n\n// extractBearerToken extracts the token value from a \"Bearer <token>\" header.\nfunc extractBearerToken(header string) string {\n\tif strings.HasPrefix(header, \"Bearer \") {\n\t\treturn header[len(\"Bearer \"):]\n\t}\n\treturn header\n}\n\n// bufferRequestBody ensures the request body can be replayed for retries.\n// If the body is nil or already replayable (GetBody is set), this is a no-op.\nfunc bufferRequestBody(req *http.Request) {\n\tif req.Body == nil || req.GetBody != nil {\n\t\treturn\n\t}\n\tbodyBytes, err := io.ReadAll(req.Body)\n\t_ = req.Body.Close()\n\tif err != nil {\n\t\treturn\n\t}\n\treq.Body = io.NopCloser(strings.NewReader(string(bodyBytes)))\n\treq.GetBody = func() (io.ReadCloser, error) {\n\t\treturn io.NopCloser(strings.NewReader(string(bodyBytes))), nil\n\t}\n}\n"
  },
  {
    "path": "internal/http/refresh_transport_test.go",
    "content": "package http\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/pkg/keyring\"\n)\n\nfunc TestRefreshTransport_PassesThroughNon401(t *testing.T) {\n\tt.Parallel()\n\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write([]byte(`{\"ok\":true}`))\n\t}))\n\tdefer server.Close()\n\n\tkeyring.MockForTesting()\n\tdefer keyring.ResetForTesting()\n\n\tkr := keyring.New()\n\t_ = kr.Set(\"test-org\", \"old-token\")\n\t_ = kr.SetRefreshToken(\"test-org\", \"refresh-token\")\n\n\tts := NewTokenSource(\"old-token\")\n\n\ttransport := &RefreshTransport{\n\t\tBase:        http.DefaultTransport,\n\t\tOrg:         \"test-org\",\n\t\tKeyring:     kr,\n\t\tTokenSource: ts,\n\t}\n\n\treq, _ := http.NewRequest(\"GET\", server.URL+\"/test\", nil)\n\treq.Header.Set(\"Authorization\", \"Bearer old-token\")\n\n\tresp, err := transport.RoundTrip(req)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"expected 200, got %d\", resp.StatusCode)\n\t}\n}\n\nfunc TestRefreshTransport_NoRefreshToken_PassesThrough401(t *testing.T) {\n\tt.Parallel()\n\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\tw.Write([]byte(`{\"message\":\"unauthorized\"}`))\n\t}))\n\tdefer server.Close()\n\n\tkeyring.MockForTesting()\n\tdefer keyring.ResetForTesting()\n\n\tkr := keyring.New()\n\t_ = kr.Set(\"test-org\", \"some-token\")\n\t// No refresh token set\n\n\tts := NewTokenSource(\"some-token\")\n\n\ttransport := &RefreshTransport{\n\t\tBase:        http.DefaultTransport,\n\t\tOrg:         \"test-org\",\n\t\tKeyring:     kr,\n\t\tTokenSource: ts,\n\t}\n\n\treq, _ := http.NewRequest(\"GET\", server.URL+\"/test\", nil)\n\treq.Header.Set(\"Authorization\", \"Bearer some-token\")\n\n\tresp, err := transport.RoundTrip(req)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusUnauthorized {\n\t\tt.Fatalf(\"expected 401 pass-through, got %d\", resp.StatusCode)\n\t}\n}\n\nfunc TestRefreshTransport_CompareAfterLock_SkipsRedundantRefresh(t *testing.T) {\n\t// This test uses t.Setenv so cannot be parallel.\n\n\tkeyring.MockForTesting()\n\tdefer keyring.ResetForTesting()\n\n\tkr := keyring.New()\n\t_ = kr.Set(\"test-org\", \"already-refreshed-token\")\n\t_ = kr.SetRefreshToken(\"test-org\", \"refresh-token\")\n\n\t// TokenSource already has the new token (simulating another goroutine\n\t// having refreshed it).\n\tts := NewTokenSource(\"already-refreshed-token\")\n\n\tvar apiCalls atomic.Int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tapiCalls.Add(1)\n\t\tauth := r.Header.Get(\"Authorization\")\n\t\tif auth == \"Bearer already-refreshed-token\" {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tw.Write([]byte(`{\"ok\":true}`))\n\t\t\treturn\n\t\t}\n\t\tw.WriteHeader(http.StatusUnauthorized)\n\t}))\n\tdefer server.Close()\n\n\t// Point BUILDKITE_HOST at a dead port so that if doRefresh is\n\t// incorrectly called, it fails fast instead of hitting a real server.\n\tt.Setenv(\"BUILDKITE_HOST\", \"127.0.0.1:1\")\n\n\ttransport := &RefreshTransport{\n\t\tBase:        http.DefaultTransport,\n\t\tOrg:         \"test-org\",\n\t\tKeyring:     kr,\n\t\tTokenSource: ts,\n\t}\n\n\t// Request with a stale token that triggers 401\n\treq, _ := http.NewRequest(\"GET\", server.URL+\"/test\", nil)\n\treq.Header.Set(\"Authorization\", \"Bearer stale-token\")\n\n\tresp, err := transport.RoundTrip(req)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatalf(\"expected 200 after compare-after-lock skip, got %d\", resp.StatusCode)\n\t}\n\t// Should have made exactly 2 API calls: the initial 401 + the retry\n\tif got := apiCalls.Load(); got != 2 {\n\t\tt.Fatalf(\"expected 2 API calls (initial + retry), got %d\", got)\n\t}\n}\n\nfunc TestRefreshTransport_DoesNotDeleteRefreshTokenOnTransientError(t *testing.T) {\n\tkeyring.MockForTesting()\n\tdefer keyring.ResetForTesting()\n\n\tkr := keyring.New()\n\t_ = kr.Set(\"test-org\", \"old-token\")\n\t_ = kr.SetRefreshToken(\"test-org\", \"my-refresh-token\")\n\n\tts := NewTokenSource(\"old-token\")\n\n\t// API server that always returns 401\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusUnauthorized)\n\t}))\n\tdefer server.Close()\n\n\t// Set BUILDKITE_HOST to a non-existent host to simulate a network error\n\t// during the refresh attempt\n\tt.Setenv(\"BUILDKITE_HOST\", \"127.0.0.1:1\") // connection refused\n\n\ttransport := &RefreshTransport{\n\t\tBase:        http.DefaultTransport,\n\t\tOrg:         \"test-org\",\n\t\tKeyring:     kr,\n\t\tTokenSource: ts,\n\t}\n\n\treq, _ := http.NewRequest(\"GET\", server.URL+\"/test\", nil)\n\treq.Header.Set(\"Authorization\", \"Bearer old-token\")\n\n\tresp, err := transport.RoundTrip(req)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\tif resp.StatusCode != http.StatusUnauthorized {\n\t\tt.Fatalf(\"expected 401 pass-through, got %d\", resp.StatusCode)\n\t}\n\n\t// The refresh token should NOT have been deleted (transient error)\n\trt, rtErr := kr.GetRefreshToken(\"test-org\")\n\tif rtErr != nil || rt != \"my-refresh-token\" {\n\t\tt.Fatalf(\"expected refresh token to be preserved after transient error, got %q err=%v\", rt, rtErr)\n\t}\n}\n\nfunc TestRefreshTransport_BuffersAndRetriesPostBody(t *testing.T) {\n\tt.Parallel()\n\n\tkeyring.MockForTesting()\n\tdefer keyring.ResetForTesting()\n\n\tkr := keyring.New()\n\t_ = kr.Set(\"test-org\", \"old-token\")\n\t_ = kr.SetRefreshToken(\"test-org\", \"refresh-token\")\n\n\tts := NewTokenSource(\"old-token\")\n\n\tvar apiCalls atomic.Int32\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tcall := apiCalls.Add(1)\n\t\tif call == 1 {\n\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\t// Verify body was replayed on retry\n\t\tbody, _ := io.ReadAll(r.Body)\n\t\t_ = body\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer server.Close()\n\n\ttransport := &RefreshTransport{\n\t\tBase:        http.DefaultTransport,\n\t\tOrg:         \"test-org\",\n\t\tKeyring:     kr,\n\t\tTokenSource: ts,\n\t}\n\n\t// Simulate a POST with a body that doesn't have GetBody set\n\tbody := `{\"query\":\"{ viewer { user { name } } }\"}`\n\treq, _ := http.NewRequest(\"POST\", server.URL+\"/graphql\", strings.NewReader(body))\n\treq.Header.Set(\"Authorization\", \"Bearer old-token\")\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t// Explicitly clear GetBody to simulate a custom reader\n\treq.GetBody = nil\n\n\t// doRefresh will fail (no real token server), but we can verify\n\t// that bufferRequestBody was called by checking the request has GetBody.\n\t// Since the refresh will fail, the 401 is returned, but the body\n\t// buffering is the important part to verify.\n\tresp, _ := transport.RoundTrip(req)\n\t_ = resp\n\n\t// Verify GetBody was set by bufferRequestBody\n\tif req.GetBody == nil {\n\t\tt.Fatal(\"expected GetBody to be set by bufferRequestBody\")\n\t}\n}\n\nfunc TestRefreshTransport_ConcurrentRequestsOnlyRefreshOnce(t *testing.T) {\n\t// This test uses t.Setenv so cannot be parallel.\n\n\tkeyring.MockForTesting()\n\tdefer keyring.ResetForTesting()\n\n\tkr := keyring.New()\n\t_ = kr.Set(\"test-org\", \"new-token\")\n\t_ = kr.SetRefreshToken(\"test-org\", \"refresh-token\")\n\n\t// TokenSource already has the refreshed token (simulating the first\n\t// goroutine having completed the refresh).\n\tts := NewTokenSource(\"new-token\")\n\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tauth := r.Header.Get(\"Authorization\")\n\t\tif auth == \"Bearer stale-token\" {\n\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\tif auth == \"Bearer new-token\" {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tw.Write([]byte(`{\"ok\":true}`))\n\t\t\treturn\n\t\t}\n\t\tw.WriteHeader(http.StatusUnauthorized)\n\t}))\n\tdefer server.Close()\n\n\t// Point BUILDKITE_HOST at a dead port so that if doRefresh is\n\t// incorrectly called (bypassing compare-after-lock), it fails.\n\tt.Setenv(\"BUILDKITE_HOST\", \"127.0.0.1:1\")\n\n\ttransport := &RefreshTransport{\n\t\tBase:        http.DefaultTransport,\n\t\tOrg:         \"test-org\",\n\t\tKeyring:     kr,\n\t\tTokenSource: ts,\n\t}\n\n\t// N goroutines hit 401 with \"stale-token\" concurrently.\n\t// All should use compare-after-lock to skip refresh and retry\n\t// with the already-refreshed \"new-token\".\n\tvar wg sync.WaitGroup\n\tresults := make([]int, 5)\n\n\tfor i := range 5 {\n\t\twg.Add(1)\n\t\tgo func(idx int) {\n\t\t\tdefer wg.Done()\n\t\t\treq, _ := http.NewRequest(\"GET\", server.URL+\"/test\", nil)\n\t\t\treq.Header.Set(\"Authorization\", \"Bearer stale-token\")\n\t\t\tresp, err := transport.RoundTrip(req)\n\t\t\tif err != nil {\n\t\t\t\tresults[idx] = -1\n\t\t\t\treturn\n\t\t\t}\n\t\t\tresults[idx] = resp.StatusCode\n\t\t}(i)\n\t}\n\n\twg.Wait()\n\n\tfor i, status := range results {\n\t\tif status != http.StatusOK {\n\t\t\tt.Errorf(\"goroutine %d: expected 200, got %d\", i, status)\n\t\t}\n\t}\n}\n\nfunc TestTokenSource_ThreadSafe(t *testing.T) {\n\tt.Parallel()\n\n\tts := NewTokenSource(\"initial\")\n\n\tvar wg sync.WaitGroup\n\tfor range 100 {\n\t\twg.Add(2)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tts.SetToken(\"updated\")\n\t\t}()\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\t_ = ts.Token()\n\t\t}()\n\t}\n\twg.Wait()\n}\n\nfunc TestIsTerminalRefreshError(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\terr      string\n\t\tterminal bool\n\t}{\n\t\t{\"token refresh error: invalid_grant - Invalid refresh token\", true},\n\t\t{\"token refresh error: unauthorized_client - Client not configured\", true},\n\t\t{\"token refresh error: invalid_client - Invalid client\", true},\n\t\t{\"refresh token request failed: dial tcp: connection refused\", false},\n\t\t{\"refresh token request failed: timeout\", false},\n\t\t{\"failed to parse token response: unexpected end of JSON\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tgot := isTerminalRefreshError(errors.New(tt.err))\n\t\tif got != tt.terminal {\n\t\t\tt.Errorf(\"isTerminalRefreshError(%q) = %v, want %v\", tt.err, got, tt.terminal)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/io/confirm.go",
    "content": "package io\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\n// Confirm prompts the user with a yes/no question.\n// Returns true if the user confirmed, false otherwise.\n//\n// IMPORTANT: Commands using Confirm() must call f.SetGlobalFlags(cmd) in PreRunE.\n// See factory.SetGlobalFlags() documentation for details.\n//\n// Usage:\n//\n//\tconfirmed, err := io.Confirm(f, \"Do the thing?\")\n//\tif err != nil {\n//\t    return err\n//\t}\n//\tif confirmed {\n//\t    // do the thing\n//\t}\nfunc Confirm(f *factory.Factory, prompt string) (bool, error) {\n\t// Check if --yes flag is set\n\tif f.SkipConfirm {\n\t\treturn true, nil\n\t}\n\n\t// Check if --no-input flag is set\n\tif f.NoInput {\n\t\treturn false, fmt.Errorf(\"interactive input required but --no-input is set\")\n\t}\n\n\tfmt.Fprintf(os.Stderr, \"%s [Y/N]: \", prompt)\n\n\tresponse, err := ReadLine()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tresponse = strings.ToLower(response)\n\treturn response == \"y\" || response == \"yes\", nil\n}\n"
  },
  {
    "path": "internal/io/input.go",
    "content": "package io\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/mattn/go-isatty\"\n)\n\n// HasDataAvailable will return whether the given Reader has data available to read\nfunc HasDataAvailable(reader io.Reader) bool {\n\tswitch f := reader.(type) {\n\tcase *os.File:\n\t\treturn !isatty.IsTerminal(f.Fd()) && !isatty.IsCygwinTerminal(f.Fd())\n\tcase *bufio.Reader:\n\t\treturn f.Size() > 0\n\tcase *strings.Reader:\n\t\treturn f.Size() > 0\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/io/pager.go",
    "content": "package io\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/anmitsu/go-shlex\"\n\t\"github.com/mattn/go-isatty\"\n)\n\n// Pager returns a writer hooked up to a pager (default: less -R) when stdout is a TTY.\n// Falls back to stdout when paging is disabled or the pager cannot run.\n// If pagerCmd is provided, it takes precedence over the PAGER environment variable.\nfunc Pager(noPager bool, pagerCmd ...string) (w io.Writer, cleanup func() error) {\n\tcleanup = func() error { return nil }\n\n\tif noPager || !isTTY() {\n\t\treturn os.Stdout, cleanup\n\t}\n\n\t// Determine pager command: explicit arg > PAGER env > default\n\tvar pagerEnv string\n\tif len(pagerCmd) > 0 && pagerCmd[0] != \"\" {\n\t\tpagerEnv = pagerCmd[0]\n\t} else {\n\t\tpagerEnv = os.Getenv(\"PAGER\")\n\t}\n\tif pagerEnv == \"\" {\n\t\tpagerEnv = \"less -R\"\n\t}\n\n\tparts, err := shlex.Split(pagerEnv, true)\n\tif err != nil || len(parts) == 0 {\n\t\treturn os.Stdout, cleanup\n\t}\n\n\tpagerBin := parts[0]\n\tpagerArgs := parts[1:]\n\n\tpagerPath, err := exec.LookPath(pagerBin)\n\tif err != nil {\n\t\treturn os.Stdout, cleanup\n\t}\n\n\tif isLessPager(pagerPath) && !hasFlag(pagerArgs, \"-R\", \"--RAW-CONTROL-CHARS\") {\n\t\tpagerArgs = append(pagerArgs, \"-R\")\n\t}\n\n\tcmd := exec.Command(pagerPath, pagerArgs...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tstdin, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn os.Stdout, cleanup\n\t}\n\n\tif err := cmd.Start(); err != nil {\n\t\tstdin.Close()\n\t\treturn os.Stdout, func() error { return nil }\n\t}\n\n\tvar once sync.Once\n\tvar cleanupErr error\n\n\tcleanup = func() error {\n\t\tonce.Do(func() {\n\t\t\tcloseErr := stdin.Close()\n\t\t\twaitErr := cmd.Wait()\n\n\t\t\tif closeErr != nil {\n\t\t\t\tcleanupErr = closeErr\n\t\t\t} else {\n\t\t\t\tcleanupErr = waitErr\n\t\t\t}\n\t\t})\n\t\treturn cleanupErr\n\t}\n\n\treturn stdin, cleanup\n}\n\nfunc isTTY() bool {\n\tif isatty.IsTerminal(os.Stdout.Fd()) {\n\t\treturn true\n\t}\n\treturn isatty.IsCygwinTerminal(os.Stdout.Fd())\n}\n\nfunc isLessPager(path string) bool {\n\tbase := filepath.Base(path)\n\treturn base == \"less\" || base == \"less.exe\"\n}\n\nfunc hasFlag(args []string, flags ...string) bool {\n\tfor _, arg := range args {\n\t\tfor _, flag := range flags {\n\t\t\tif arg == flag || strings.HasPrefix(arg, flag+\"=\") {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/io/pager_test.go",
    "content": "package io\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"testing\"\n)\n\nfunc TestPagerReturnsStdoutWhenNoPagerTrue(t *testing.T) {\n\tw, cleanup := Pager(true)\n\tdefer cleanup()\n\n\tif w != os.Stdout {\n\t\tt.Errorf(\"expected os.Stdout when noPager=true, got %v\", w)\n\t}\n}\n\nfunc TestPagerReturnsStdoutWhenNotTTY(t *testing.T) {\n\tw, cleanup := Pager(false)\n\tdefer cleanup()\n\n\tif w != os.Stdout {\n\t\tt.Errorf(\"expected os.Stdout when not a TTY, got %v\", w)\n\t}\n}\n\nfunc TestPagerReturnsStdoutWhenPagerNotFound(t *testing.T) {\n\toriginalPager := os.Getenv(\"PAGER\")\n\tdefer os.Setenv(\"PAGER\", originalPager)\n\n\tos.Setenv(\"PAGER\", \"nonexistent-pager-command-12345\")\n\n\tw, cleanup := Pager(false)\n\tdefer cleanup()\n\n\tif w != os.Stdout {\n\t\tt.Errorf(\"expected os.Stdout when pager not found, got %v\", w)\n\t}\n}\n\nfunc TestPagerReturnsStdoutWhenPagerEnvMalformed(t *testing.T) {\n\toriginalPager := os.Getenv(\"PAGER\")\n\tdefer os.Setenv(\"PAGER\", originalPager)\n\n\tos.Setenv(\"PAGER\", \"less \\\"unclosed\")\n\n\tw, cleanup := Pager(false)\n\tdefer cleanup()\n\n\tif w != os.Stdout {\n\t\tt.Errorf(\"expected os.Stdout when PAGER env is malformed, got %v\", w)\n\t}\n}\n\nfunc TestPagerReturnsStdoutWhenPagerEnvEmpty(t *testing.T) {\n\toriginalPager := os.Getenv(\"PAGER\")\n\tdefer os.Setenv(\"PAGER\", originalPager)\n\n\tos.Setenv(\"PAGER\", \"\")\n\n\tw, cleanup := Pager(false)\n\tdefer cleanup()\n\n\tif w != os.Stdout {\n\t\tt.Errorf(\"expected os.Stdout, got %v\", w)\n\t}\n}\n\nfunc TestPagerCleanupIsIdempotent(t *testing.T) {\n\toriginalPager := os.Getenv(\"PAGER\")\n\tdefer os.Setenv(\"PAGER\", originalPager)\n\n\tos.Setenv(\"PAGER\", \"nonexistent-pager\")\n\n\t_, cleanup := Pager(false)\n\n\terr1 := cleanup()\n\terr2 := cleanup()\n\terr3 := cleanup()\n\n\tif err1 != nil {\n\t\tt.Errorf(\"first cleanup returned error: %v\", err1)\n\t}\n\tif err2 != nil {\n\t\tt.Errorf(\"second cleanup returned error: %v\", err2)\n\t}\n\tif err3 != nil {\n\t\tt.Errorf(\"third cleanup returned error: %v\", err3)\n\t}\n}\n\nfunc TestPagerWithCatCommand(t *testing.T) {\n\tif _, err := exec.LookPath(\"cat\"); err != nil {\n\t\tt.Skip(\"cat command not found\")\n\t}\n\n\toriginalPager := os.Getenv(\"PAGER\")\n\tdefer os.Setenv(\"PAGER\", originalPager)\n\n\tos.Setenv(\"PAGER\", \"cat\")\n\n\tw, cleanup := Pager(false)\n\tdefer cleanup()\n\n\tif w != os.Stdout {\n\t\tt.Errorf(\"expected os.Stdout in non-TTY, got %v\", w)\n\t}\n}\n\nfunc TestIsLessPager(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpath     string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"Unix less\",\n\t\t\tpath:     \"/usr/bin/less\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"Windows less.exe\",\n\t\t\tpath:     \"less.exe\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"less in current dir\",\n\t\t\tpath:     \"./less\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"not less - cat\",\n\t\t\tpath:     \"/usr/bin/cat\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"not less - more\",\n\t\t\tpath:     \"/usr/bin/more\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"substring match should fail\",\n\t\t\tpath:     \"/usr/bin/lessjs\",\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := isLessPager(tt.path)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"isLessPager(%q) = %v, expected %v\", tt.path, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHasFlag(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\targs     []string\n\t\tflags    []string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"flag present\",\n\t\t\targs:     []string{\"-R\", \"-X\"},\n\t\t\tflags:    []string{\"-R\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"flag not present\",\n\t\t\targs:     []string{\"-X\", \"-F\"},\n\t\t\tflags:    []string{\"-R\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple flags, one matches\",\n\t\t\targs:     []string{\"-R\", \"-X\"},\n\t\t\tflags:    []string{\"-R\", \"--RAW-CONTROL-CHARS\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"long flag matches\",\n\t\t\targs:     []string{\"--RAW-CONTROL-CHARS\"},\n\t\t\tflags:    []string{\"-R\", \"--RAW-CONTROL-CHARS\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"flag with value using equals\",\n\t\t\targs:     []string{\"--option=value\"},\n\t\t\tflags:    []string{\"--option\"},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty args\",\n\t\t\targs:     []string{},\n\t\t\tflags:    []string{\"-R\"},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty flags\",\n\t\t\targs:     []string{\"-R\"},\n\t\t\tflags:    []string{},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"substring should not match\",\n\t\t\targs:     []string{\"-RX\"},\n\t\t\tflags:    []string{\"-R\"},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := hasFlag(tt.args, tt.flags...)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"hasFlag(%v, %v) = %v, expected %v\", tt.args, tt.flags, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPagerAddsRawFlagToLess(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tpagerPath     string\n\t\tinitialArgs   []string\n\t\tshouldAddFlag bool\n\t}{\n\t\t{\n\t\t\tname:          \"less without -R should add it\",\n\t\t\tpagerPath:     \"/usr/bin/less\",\n\t\t\tinitialArgs:   []string{\"-X\"},\n\t\t\tshouldAddFlag: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"less with -R should not add it\",\n\t\t\tpagerPath:     \"/usr/bin/less\",\n\t\t\tinitialArgs:   []string{\"-R\", \"-X\"},\n\t\t\tshouldAddFlag: false,\n\t\t},\n\t\t{\n\t\t\tname:          \"less with --RAW-CONTROL-CHARS should not add -R\",\n\t\t\tpagerPath:     \"/usr/bin/less\",\n\t\t\tinitialArgs:   []string{\"--RAW-CONTROL-CHARS\"},\n\t\t\tshouldAddFlag: false,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-less pager should not add -R\",\n\t\t\tpagerPath:     \"/usr/bin/cat\",\n\t\t\tinitialArgs:   []string{},\n\t\t\tshouldAddFlag: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tshouldAdd := isLessPager(tt.pagerPath) && !hasFlag(tt.initialArgs, \"-R\", \"--RAW-CONTROL-CHARS\")\n\t\t\tif shouldAdd != tt.shouldAddFlag {\n\t\t\t\tt.Errorf(\"expected shouldAdd=%v, got %v\", tt.shouldAddFlag, shouldAdd)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPagerWriteAndCleanup(t *testing.T) {\n\t// Use cat as a simple pager that will work in tests\n\tif _, err := exec.LookPath(\"cat\"); err != nil {\n\t\tt.Skip(\"cat command not found\")\n\t}\n\n\toriginalPager := os.Getenv(\"PAGER\")\n\tdefer os.Setenv(\"PAGER\", originalPager)\n\n\tos.Setenv(\"PAGER\", \"cat\")\n\n\tw, cleanup := Pager(false)\n\tdefer cleanup()\n\n\tif w == nil {\n\t\tt.Fatal(\"expected non-nil writer\")\n\t}\n\n\t// Test that cleanup doesn't return an error\n\tif err := cleanup(); err != nil {\n\t\tt.Errorf(\"cleanup returned error: %v\", err)\n\t}\n}\n\nfunc TestPagerCleanupAfterFailedStart(t *testing.T) {\n\toriginalPager := os.Getenv(\"PAGER\")\n\tdefer os.Setenv(\"PAGER\", originalPager)\n\n\tos.Setenv(\"PAGER\", \"false\")\n\n\tw, cleanup := Pager(false)\n\n\tif w != os.Stdout {\n\t\tt.Errorf(\"expected os.Stdout, got %v\", w)\n\t}\n\n\tif err := cleanup(); err != nil {\n\t\tt.Errorf(\"cleanup returned error: %v\", err)\n\t}\n\tif err := cleanup(); err != nil {\n\t\tt.Errorf(\"second cleanup returned error: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "internal/io/progress.go",
    "content": "package io\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc ProgressBar(completed, total, width int) string {\n\tif width <= 0 {\n\t\treturn \"[]\"\n\t}\n\tif total <= 0 {\n\t\treturn \"[\" + strings.Repeat(\"░\", width) + \"]\"\n\t}\n\n\tif completed < 0 {\n\t\tcompleted = 0\n\t}\n\n\tfilled := min(completed*width/total, width)\n\n\treturn \"[\" + strings.Repeat(\"█\", filled) + strings.Repeat(\"░\", width-filled) + \"]\"\n}\n\nfunc ProgressLine(label string, completed, total, succeeded, failed, barWidth int) string {\n\tif total == 0 {\n\t\treturn fmt.Sprintf(\"%s [no items]\", label)\n\t}\n\n\tbar := ProgressBar(completed, total, barWidth)\n\tpercent := min(completed*100/total, 100)\n\n\treturn fmt.Sprintf(\"%s %s %3d%% %d/%d succeeded:%d failed:%d\", label, bar, percent, completed, total, succeeded, failed)\n}\n"
  },
  {
    "path": "internal/io/progress_test.go",
    "content": "package io\n\nimport \"testing\"\n\nfunc TestProgressBar(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname      string\n\t\tcompleted int\n\t\ttotal     int\n\t\twidth     int\n\t\texpected  string\n\t}{\n\t\t{\"half filled\", 5, 10, 10, \"[█████░░░░░]\"},\n\t\t{\"empty with zero total\", 0, 0, 4, \"[░░░░]\"},\n\t\t{\"full bar\", 10, 10, 10, \"[██████████]\"},\n\t\t{\"overflow clamped\", 15, 10, 10, \"[██████████]\"},\n\t\t{\"zero width\", 5, 10, 0, \"[]\"},\n\t\t{\"negative completed\", -5, 10, 10, \"[░░░░░░░░░░]\"},\n\t\t{\"one char width\", 1, 2, 1, \"[░]\"},\n\t\t{\"complete one char\", 2, 2, 1, \"[█]\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := ProgressBar(tt.completed, tt.total, tt.width)\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"ProgressBar(%d, %d, %d) = %q, want %q\",\n\t\t\t\t\ttt.completed, tt.total, tt.width, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProgressLine(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname      string\n\t\tlabel     string\n\t\tcompleted int\n\t\ttotal     int\n\t\tsucceeded int\n\t\tfailed    int\n\t\tbarWidth  int\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\t\"partial progress\",\n\t\t\t\"Work\", 3, 10, 2, 1, 6,\n\t\t\t\"Work [█░░░░░]  30% 3/10 succeeded:2 failed:1\",\n\t\t},\n\t\t{\n\t\t\t\"no items\",\n\t\t\t\"Work\", 0, 0, 0, 0, 6,\n\t\t\t\"Work [no items]\",\n\t\t},\n\t\t{\n\t\t\t\"complete\",\n\t\t\t\"Task\", 10, 10, 10, 0, 10,\n\t\t\t\"Task [██████████] 100% 10/10 succeeded:10 failed:0\",\n\t\t},\n\t\t{\n\t\t\t\"all failed\",\n\t\t\t\"Ops\", 5, 5, 0, 5, 5,\n\t\t\t\"Ops [█████] 100% 5/5 succeeded:0 failed:5\",\n\t\t},\n\t\t{\n\t\t\t\"mixed results at 50%\",\n\t\t\t\"Deploy\", 5, 10, 3, 2, 8,\n\t\t\t\"Deploy [████░░░░]  50% 5/10 succeeded:3 failed:2\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := ProgressLine(tt.label, tt.completed, tt.total, tt.succeeded, tt.failed, tt.barWidth)\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"got %q, want %q\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/io/prompt.go",
    "content": "package io\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/mattn/go-runewidth\"\n)\n\nconst (\n\ttypeOrganizationMessage = \"Pick an organization\"\n\ttypePipelineMessage     = \"Select a pipeline\"\n)\n\n// PromptForOne will show the list of options to the user, allowing them to select one to return.\n// It's possible for them to choose none or cancel the selection, resulting in an error.\n// If noInput is true, it will fail instead of prompting.\n// If there's no TTY available, it will also fail instead of prompting.\n//\n// For global flag support requirements, see the Confirm() function documentation.\nfunc PromptForOne(resource string, options []string, noInput bool) (string, error) {\n\tif noInput {\n\t\treturn \"\", fmt.Errorf(\"interactive input required but --no-input flag is set\")\n\t}\n\n\t// Check if we have a TTY available - if not, treat it as if noInput is true\n\tif !isTerminal(os.Stdin) {\n\t\treturn \"\", fmt.Errorf(\"interactive input required but no TTY available\")\n\t}\n\n\tvar message string\n\tswitch resource {\n\tcase \"pipeline\":\n\t\tmessage = typePipelineMessage\n\tcase \"organization\":\n\t\tmessage = typeOrganizationMessage\n\tdefault:\n\t\tmessage = \"Please select one of the options below\"\n\t}\n\n\tif len(options) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no options available\")\n\t}\n\n\tfmt.Printf(\"%s:\\n\", message)\n\tfor i, option := range options {\n\t\tfmt.Printf(\"  %d. %s\\n\", i+1, option)\n\t}\n\tprompt := fmt.Sprintf(\"Enter number (1-%d): \", len(options))\n\tfmt.Print(prompt)\n\n\tresponse, err := ReadLine()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tnum, err := strconv.Atoi(response)\n\tif err != nil || num < 1 || num > len(options) {\n\t\treturn \"\", fmt.Errorf(\"invalid selection\")\n\t}\n\n\tclearPreviousLines(os.Stdout, renderedLineCount(message, options, prompt+response, terminalWidth(os.Stdout)))\n\n\treturn options[num-1], nil\n}\n\nfunc renderedLineCount(message string, options []string, prompt string, width int) int {\n\tlines := wrappedLineCount(message+\":\", width)\n\tfor i, option := range options {\n\t\tlines += wrappedLineCount(fmt.Sprintf(\"  %d. %s\", i+1, option), width)\n\t}\n\tlines += wrappedLineCount(prompt, width)\n\treturn lines\n}\n\nfunc wrappedLineCount(s string, width int) int {\n\tif width <= 0 {\n\t\treturn 1\n\t}\n\n\tlineWidth := runewidth.StringWidth(s)\n\tif lineWidth == 0 {\n\t\treturn 1\n\t}\n\n\treturn (lineWidth-1)/width + 1\n}\n\n// PromptForInput prompts the user to enter a string value.\n// If a default value is provided, it will be shown in brackets and used if the user presses enter.\n// If noInput is true, it will return the default value or an error if no default is provided.\nfunc PromptForInput(prompt, defaultVal string, noInput bool) (string, error) {\n\tif noInput {\n\t\tif defaultVal != \"\" {\n\t\t\treturn defaultVal, nil\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"interactive input required but --no-input flag is set\")\n\t}\n\n\tif defaultVal != \"\" {\n\t\tfmt.Printf(\"%s [%s]: \", prompt, defaultVal)\n\t} else {\n\t\tfmt.Printf(\"%s: \", prompt)\n\t}\n\n\tresponse, err := ReadLine()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresponse = strings.TrimSpace(response)\n\tif response == \"\" && defaultVal != \"\" {\n\t\treturn defaultVal, nil\n\t}\n\n\tif response == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no value provided for %s\", prompt)\n\t}\n\n\treturn response, nil\n}\n"
  },
  {
    "path": "internal/io/prompt_test.go",
    "content": "package io\n\nimport \"testing\"\n\nfunc TestWrappedLineCount(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname  string\n\t\tinput string\n\t\twidth int\n\t\twant  int\n\t}{\n\t\t{name: \"empty string\", input: \"\", width: 80, want: 1},\n\t\t{name: \"fits on one line\", input: \"hello\", width: 80, want: 1},\n\t\t{name: \"wraps across two lines\", input: \"1234567890\", width: 5, want: 2},\n\t\t{name: \"invalid width falls back to one line\", input: \"hello\", width: 0, want: 1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tif got := wrappedLineCount(tt.input, tt.width); got != tt.want {\n\t\t\t\tt.Fatalf(\"wrappedLineCount(%q, %d) = %d, want %d\", tt.input, tt.width, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRenderedLineCount(t *testing.T) {\n\tt.Parallel()\n\n\tgot := renderedLineCount(\"Select a pipeline\", []string{\"first\", \"second\"}, \"Enter number (1-2): 2\", 80)\n\twant := 4\n\tif got != want {\n\t\tt.Fatalf(\"renderedLineCount() = %d, want %d\", got, want)\n\t}\n}\n"
  },
  {
    "path": "internal/io/readline.go",
    "content": "package io\n\nimport (\n\t\"bufio\"\n\t\"os\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"golang.org/x/term\"\n)\n\n// ReadLine reads a line of input from stdin with terminal support.\n// If running in a TTY, it uses x/term for better line editing (backspace, arrows, etc.).\n// If not in a TTY (e.g., piped input), it falls back to bufio.\nfunc ReadLine() (string, error) {\n\tfd := int(os.Stdin.Fd())\n\n\t// Check if we're in a TTY\n\tif !term.IsTerminal(fd) {\n\t\t// Not a TTY, use simple bufio\n\t\treader := bufio.NewReader(os.Stdin)\n\t\tline, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn strings.TrimSpace(line), nil\n\t}\n\n\t// TTY - use x/term for better editing\n\toldState, err := term.MakeRaw(fd)\n\tif err != nil {\n\t\t// Fallback to bufio if raw mode fails\n\t\treader := bufio.NewReader(os.Stdin)\n\t\tline, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn strings.TrimSpace(line), nil\n\t}\n\n\tterminal := term.NewTerminal(os.Stdin, \"\")\n\tline, err := terminal.ReadLine()\n\t_ = term.Restore(fd, oldState)\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn strings.TrimSpace(line), nil\n}\n\n// ReadPassword reads a password from stdin without echoing.\n// This is a convenience wrapper around term.ReadPassword.\nfunc ReadPassword() (string, error) {\n\tpasswordBytes, err := term.ReadPassword(int(syscall.Stdin))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(passwordBytes), nil\n}\n"
  },
  {
    "path": "internal/io/spinner.go",
    "content": "package io\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/mattn/go-isatty\"\n)\n\nfunc SpinWhile(f *factory.Factory, name string, action func() error) error {\n\t// If quiet mode is on or not a terminal, just run the action\n\tif f.Quiet || !isatty.IsTerminal(os.Stdout.Fd()) {\n\t\treturn action()\n\t}\n\n\tdone := make(chan struct{})\n\tfinished := make(chan struct{})\n\n\tgo func() {\n\t\tticker := time.NewTicker(200 * time.Millisecond)\n\t\tdefer ticker.Stop()\n\n\t\ti := 0\n\t\tchars := []string{\".  \", \".. \", \"...\"}\n\t\tmaxLen := 0\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\t// Clear the line by overwriting with spaces\n\t\t\t\tclearLine := \"\\r\" + fmt.Sprintf(\"%*s\", maxLen, \"\") + \"\\r\"\n\t\t\t\tfmt.Fprint(os.Stderr, clearLine)\n\t\t\t\tclose(finished)\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\tline := fmt.Sprintf(\"%s %s\", name, chars[i%len(chars)])\n\t\t\t\tif len(line) > maxLen {\n\t\t\t\t\tmaxLen = len(line)\n\t\t\t\t}\n\t\t\t\tfmt.Fprintf(os.Stderr, \"\\r%s\", line)\n\t\t\t\ti++\n\t\t\t}\n\t\t}\n\t}()\n\n\tactionErr := action()\n\tclose(done)\n\t<-finished // Wait for spinner to finish clearing\n\n\treturn actionErr\n}\n"
  },
  {
    "path": "internal/io/spinner_test.go",
    "content": "package io\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\t\"github.com/mattn/go-isatty\"\n)\n\nfunc TestSpinWhileWithoutTTY(t *testing.T) {\n\t// Test that SpinWhile works without TTY\n\tactionCalled := false\n\tf := &factory.Factory{}\n\terr := SpinWhile(f, \"Test action\", func() error {\n\t\tactionCalled = true\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"SpinWhile should not return error: %v\", err)\n\t}\n\n\tif !actionCalled {\n\t\tt.Error(\"Action should have been called\")\n\t}\n}\n\nfunc TestSpinWhileActionIsExecuted(t *testing.T) {\n\t// Test that the action is always executed regardless of TTY status\n\tcounter := 0\n\tf := &factory.Factory{}\n\terr := SpinWhile(f, \"Test action\", func() error {\n\t\tcounter++\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"SpinWhile should not return error: %v\", err)\n\t}\n\n\tif counter != 1 {\n\t\tt.Errorf(\"Action should have been called exactly once, got %d\", counter)\n\t}\n}\n\nfunc TestSpinWhileWithError(t *testing.T) {\n\t// Test SpinWhile when action panics or has issues\n\tactionCalled := false\n\tf := &factory.Factory{}\n\terr := SpinWhile(f, \"Test action with panic recovery\", func() error {\n\t\tactionCalled = true\n\t\t// Don't actually panic in test, just test normal flow\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"SpinWhile should not return error for normal action: %v\", err)\n\t}\n\n\tif !actionCalled {\n\t\tt.Error(\"Action should have been called\")\n\t}\n}\n\nfunc TestSpinWhileTTYDetection(t *testing.T) {\n\t// Test that TTY detection works as expected\n\t// This test documents the behavior rather than forcing specific outcomes\n\tisTTY := isatty.IsTerminal(os.Stdout.Fd())\n\n\tactionCalled := false\n\tf := &factory.Factory{}\n\terr := SpinWhile(f, \"TTY detection test\", func() error {\n\t\tactionCalled = true\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"SpinWhile should not return error: %v\", err)\n\t}\n\n\tif !actionCalled {\n\t\tt.Error(\"Action should have been called regardless of TTY status\")\n\t}\n\n\t// Document the current TTY status for debugging\n\tt.Logf(\"Current TTY status: %v\", isTTY)\n}\n\nfunc TestSpinWhileQuiet(t *testing.T) {\n\t// Test that SpinWhile works with Quiet mode\n\tactionCalled := false\n\tf := &factory.Factory{Quiet: true}\n\terr := SpinWhile(f, \"Test action\", func() error {\n\t\tactionCalled = true\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"SpinWhile should not return error: %v\", err)\n\t}\n\n\tif !actionCalled {\n\t\tt.Error(\"Action should have been called\")\n\t}\n}\n"
  },
  {
    "path": "internal/io/terminal.go",
    "content": "package io\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/mattn/go-isatty\"\n\t\"golang.org/x/term\"\n)\n\nconst clearPreviousLineANSI = \"\\x1b[1A\\r\\x1b[2K\"\n\nfunc isTerminal(f *os.File) bool {\n\treturn isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())\n}\n\nfunc terminalWidth(f *os.File) int {\n\twidth, _, err := term.GetSize(int(f.Fd()))\n\tif err != nil || width <= 0 {\n\t\treturn 80\n\t}\n\treturn width\n}\n\nfunc clearPreviousLines(f *os.File, lines int) {\n\tif lines <= 0 || !isTerminal(f) {\n\t\treturn\n\t}\n\n\tfor range lines {\n\t\tfmt.Fprint(f, clearPreviousLineANSI)\n\t}\n}\n"
  },
  {
    "path": "internal/job/view.go",
    "content": "package job\n\nimport (\n\t\"github.com/buildkite/cli/v3/internal/build/view\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype Job buildkite.Job\n\n// JobSummary renders a job summary\nfunc JobSummary(job Job) string {\n\treturn job.Summarise()\n}\n\n// Summarise renders a summary of the job\nfunc (j Job) Summarise() string {\n\t// Convert the internal Job type back to buildkite.Job for rendering\n\tbkJob := buildkite.Job(j)\n\treturn view.RenderJobSummary(bkJob)\n}\n"
  },
  {
    "path": "internal/organization/organization.graphql",
    "content": "query GetOrganizationID ($slug: ID!) {\n  organization(slug: $slug){\n    id\n  }\n}\n\n"
  },
  {
    "path": "internal/pipeline/pipeline.go",
    "content": "package pipeline\n\n// Pipeline is a struct containing information about a pipeline for a resolver to return\ntype Pipeline struct {\n\tName string\n\tOrg  string\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/cli.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n)\n\nfunc ResolveFromPositionalArgument(args []string, index int, conf *config.Config) PipelineResolverFn {\n\treturn func(context.Context) (*pipeline.Pipeline, error) {\n\t\t// if args does not have values, skip this resolver\n\t\tif len(args) < 1 {\n\t\t\treturn nil, nil\n\t\t}\n\t\t// if the index is out of bounds\n\t\tif (len(args) - 1) < index {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\torg, name := parsePipelineArg(args[index], conf)\n\n\t\t// if we get here, we should be able to parse the value and return an error if not\n\t\t// this is because a user has explicitly given an input value for us to use - we shoulnt ignore it on error\n\t\tif org == \"\" || name == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse the input pipeline argument: \\\"%s\\\"\", args[index])\n\t\t}\n\n\t\treturn &pipeline.Pipeline{Name: name, Org: org}, nil\n\t}\n}\n\n// parsePipelineArg resolve an input string in varying formats to an organization and pipeline pair\n// some example input formats are:\n// - a web URL: https://buildkite.com/<org>/<pipeline slug>/builds/...\n// - a slug: <org>/<pipeline slug>\n// - a pipeline slug by itself\nfunc parsePipelineArg(arg string, conf *config.Config) (org, pipeline string) {\n\tpipelineIsURL := strings.Contains(arg, \":\")\n\tpipelineIsSlug := !pipelineIsURL && strings.Contains(arg, \"/\")\n\n\tif pipelineIsURL {\n\t\turl, err := url.Parse(arg)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\"\n\t\t}\n\t\t// eg: url.Path = /buildkite/buildkite-cli\n\t\tpart := strings.Split(url.Path, \"/\")\n\t\tif len(part) < 3 {\n\t\t\treturn \"\", \"\"\n\t\t}\n\t\torg, pipeline = part[1], part[2]\n\t} else if pipelineIsSlug {\n\t\tpart := strings.Split(arg, \"/\")\n\t\tif len(part) < 2 {\n\t\t\treturn \"\", \"\"\n\t\t}\n\t\torg, pipeline = part[0], part[1]\n\t} else {\n\t\torg = conf.OrganizationSlug()\n\t\tpipeline = arg\n\t}\n\treturn org, pipeline\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/cli_test.go",
    "content": "package resolver_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc TestParsePipelineArg(t *testing.T) {\n\tt.Parallel()\n\n\ttestcases := map[string]struct {\n\t\turl, org, pipeline string\n\t}{\n\t\t\"org_pipeline_slug\": {\n\t\t\turl:      \"buildkite/cli\",\n\t\t\torg:      \"buildkite\",\n\t\t\tpipeline: \"cli\",\n\t\t},\n\t\t\"pipeline_slug\": {\n\t\t\turl:      \"abcd\",\n\t\t\torg:      \"testing\",\n\t\t\tpipeline: \"abcd\",\n\t\t},\n\t\t\"url\": {\n\t\t\turl:      \"https://buildkite.com/buildkite/buildkite-cli\",\n\t\t\torg:      \"buildkite\",\n\t\t\tpipeline: \"buildkite-cli\",\n\t\t},\n\t}\n\n\tfor name, testcase := range testcases {\n\t\ttestcase := testcase\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\t\tconf.SelectOrganization(\"testing\", true)\n\t\t\tf := resolver.ResolveFromPositionalArgument([]string{testcase.url}, 0, conf)\n\t\t\tpipeline, err := f(context.Background())\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tif pipeline.Org != testcase.org {\n\t\t\t\tt.Error(\"parsed organization slug did not match expected\")\n\t\t\t}\n\t\t\tif pipeline.Name != testcase.pipeline {\n\t\t\t\tt.Error(\"parsed pipeline name did not match expected\")\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"pipeline slug uses configured org\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\tconf.SelectOrganization(\"testing\", true)\n\t\tf := resolver.ResolveFromPositionalArgument([]string{\"my-pipeline\"}, 0, conf)\n\t\tpipeline, err := f(context.Background())\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif pipeline.Org != \"testing\" {\n\t\t\tt.Errorf(\"expected org to be 'testing', got '%s'\", pipeline.Org)\n\t\t}\n\t\tif pipeline.Name != \"my-pipeline\" {\n\t\t\tt.Errorf(\"expected pipeline to be 'my-pipeline', got '%s'\", pipeline.Name)\n\t\t}\n\t})\n\n\tt.Run(\"Returns error if failed parsing\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\tconf.SelectOrganization(\"testing\", true)\n\t\tf := resolver.ResolveFromPositionalArgument([]string{\"https://buildkite.com/\"}, 0, conf)\n\t\tpipeline, err := f(context.Background())\n\t\tif err == nil {\n\t\t\tt.Error(\"Should have failed parsing pipeline\")\n\t\t}\n\t\tif pipeline != nil {\n\t\t\tt.Error(\"No pipeline should be returned\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/config.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n)\n\nfunc ResolveFromConfig(conf *config.Config, picker PipelinePicker) PipelineResolverFn {\n\treturn func(context.Context) (*pipeline.Pipeline, error) {\n\t\tpipelines := conf.PreferredPipelines()\n\n\t\tif len(pipelines) == 0 {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn picker(pipelines), nil\n\t}\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/config_test.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc TestResolvePipelineFromConfig(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no pipelines from config\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\tresolve := ResolveFromConfig(conf, PassthruPicker)\n\t\tselected, err := resolve(context.Background())\n\t\tif err != nil {\n\t\t\tt.Errorf(\"failed to resolve from config\")\n\t\t}\n\n\t\tif selected != nil {\n\t\t\tt.Errorf(\"pipeline must be nil\")\n\t\t}\n\t})\n\n\tt.Run(\"Resolve to one pipeline\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tpipelines := []pipeline.Pipeline{{Name: \"pipeline1\"}}\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\tconf.SetPreferredPipelines(pipelines)\n\t\tresolve := ResolveFromConfig(conf, PassthruPicker)\n\t\tselected, err := resolve(context.Background())\n\t\tif err != nil {\n\t\t\tt.Errorf(\"failed to resolve from config\")\n\t\t}\n\n\t\tif selected == nil {\n\t\t\tt.Errorf(\"pipeline must not be nil\")\n\t\t}\n\n\t\tif selected != nil && selected.Name != pipelines[0].Name {\n\t\t\tt.Errorf(\"pipeline name must be pipeline1\")\n\t\t}\n\t})\n\n\tt.Run(\"Resolve to many pipelines\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tpipelines := []pipeline.Pipeline{{Name: \"pipeline1\"}, {Name: \"pipeline2\"}, {Name: \"pipeline3\"}}\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\tconf.SetPreferredPipelines(pipelines)\n\t\tresolve := ResolveFromConfig(conf, PassthruPicker)\n\t\tselected, err := resolve(context.Background())\n\t\tif err != nil {\n\t\t\tt.Errorf(\"failed to resolve from config\")\n\t\t}\n\n\t\tif selected == nil {\n\t\t\tt.Errorf(\"pipeline must not be nil\")\n\t\t}\n\n\t\tif selected != nil && selected.Name != pipelines[0].Name {\n\t\t\tt.Errorf(\"pipeline name should resolve temporarily to pipeline1\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/flag.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n)\n\nfunc ResolveFromFlag(flag string, conf *config.Config) PipelineResolverFn {\n\treturn func(context.Context) (*pipeline.Pipeline, error) {\n\t\t// if the flag is empty, pass through\n\t\tif flag == \"\" {\n\t\t\treturn nil, nil\n\t\t}\n\t\torg, name := parsePipelineArg(flag, conf)\n\n\t\t// if we get here, we should be able to parse the value and return an error if not\n\t\t// this is because a user has explicitly given an input value for us to use - we shoulnt ignore it on error\n\t\tif org == \"\" || name == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"unable to parse the input pipeline argument: \\\"%s\\\"\", flag)\n\t\t}\n\n\t\treturn &pipeline.Pipeline{Name: name, Org: org}, nil\n\t}\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/flag_test.go",
    "content": "package resolver_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc TestResolveFromFlag(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty flag returns nil\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\tconf.SelectOrganization(\"testing\", true)\n\t\tf := resolver.ResolveFromFlag(\"\", conf)\n\t\tpipeline, err := f(context.Background())\n\t\tif err != nil {\n\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif pipeline != nil {\n\t\t\tt.Error(\"expected nil pipeline for empty flag\")\n\t\t}\n\t})\n\n\tt.Run(\"pipeline slug uses config org\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\tconf.SelectOrganization(\"testing\", true)\n\t\tf := resolver.ResolveFromFlag(\"my-pipeline\", conf)\n\t\tpipeline, err := f(context.Background())\n\t\tif err != nil {\n\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif pipeline.Org != \"testing\" {\n\t\t\tt.Errorf(\"expected org 'testing', got '%s'\", pipeline.Org)\n\t\t}\n\t\tif pipeline.Name != \"my-pipeline\" {\n\t\t\tt.Errorf(\"expected pipeline 'my-pipeline', got '%s'\", pipeline.Name)\n\t\t}\n\t})\n\n\tt.Run(\"org/pipeline slug extracts org\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tconf := config.New(afero.NewMemMapFs(), nil)\n\t\tconf.SelectOrganization(\"testing\", true)\n\t\tf := resolver.ResolveFromFlag(\"other-org/my-pipeline\", conf)\n\t\tpipeline, err := f(context.Background())\n\t\tif err != nil {\n\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif pipeline.Org != \"other-org\" {\n\t\t\tt.Errorf(\"expected org 'other-org', got '%s'\", pipeline.Org)\n\t\t}\n\t\tif pipeline.Name != \"my-pipeline\" {\n\t\t\tt.Errorf(\"expected pipeline 'my-pipeline', got '%s'\", pipeline.Name)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/picker.go",
    "content": "package resolver\n\nimport (\n\t\"slices\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n)\n\n// PipelinePicker is a function used to pick a pipeline from a list.\n//\n// It is indended to be used from pipeline resolvers that resolve multiple pipelines.\ntype PipelinePicker func([]pipeline.Pipeline) *pipeline.Pipeline\n\nfunc PassthruPicker(p []pipeline.Pipeline) *pipeline.Pipeline {\n\treturn &p[0]\n}\n\n// PickOneWithFactory returns a picker that uses the factory's NoInput flag.\n// When multiple pipelines are found and NoInput is true, it fails instead of prompting.\nfunc PickOneWithFactory(f *factory.Factory) PipelinePicker {\n\treturn func(pipelines []pipeline.Pipeline) *pipeline.Pipeline {\n\t\tif len(pipelines) == 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\t// no need to prompt for only one option\n\t\tif len(pipelines) == 1 {\n\t\t\treturn &pipelines[0]\n\t\t}\n\n\t\tnames := make([]string, len(pipelines))\n\t\tfor i, p := range pipelines {\n\t\t\tnames[i] = p.Name\n\t\t}\n\n\t\tchosen, err := io.PromptForOne(\"pipeline\", names, f.NoInput)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Find which pipeline was chosen\n\t\tindex := slices.IndexFunc(pipelines, func(p pipeline.Pipeline) bool {\n\t\t\treturn p.Name == chosen\n\t\t})\n\n\t\tif index < 0 {\n\t\t\t// Shouldn't happen, just in case\n\t\t\treturn nil\n\t\t}\n\n\t\treturn &pipelines[index]\n\t}\n}\n\n// CachedPicker returns a PipelinePicker that saves the given pipelines to local config as well as running the provider\n// picker.\nfunc CachedPicker(conf *config.Config, picker PipelinePicker) PipelinePicker {\n\treturn func(pipelines []pipeline.Pipeline) *pipeline.Pipeline {\n\t\t// run the picker first because we want to put the chosen on at the top of the saved list\n\t\tchosen := picker(pipelines)\n\t\t// if chosen is nil, either there were no pipelines to begin with, or the user cancelled the picker, so we\n\t\t// probably shouldnt save them to config\n\t\tif chosen == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\t// pointers and slices are getting in our way here, so copy the current pipeline pointed to by chosen into a\n\t\t// temporary variable to later return, as the value chosen points to is going to change when we rearrange the\n\t\t// pipelines slice\n\t\ttmp := *chosen\n\t\tindex := slices.IndexFunc(pipelines, func(p pipeline.Pipeline) bool {\n\t\t\treturn tmp.Name == p.Name\n\t\t})\n\t\tpipelines[0], pipelines[index] = tmp, pipelines[0]\n\n\t\t// best-effort: cache the selection if possible\n\t\t_ = conf.SetPreferredPipelines(pipelines)\n\n\t\treturn &tmp\n\t}\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/picker_test.go",
    "content": "package resolver_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/spf13/afero\"\n)\n\ntype savedConfig struct {\n\tSelectedOrg string   `yaml:\"selected_org\"`\n\tPipelines   []string `yaml:\"pipelines\"`\n}\n\nfunc readSavedConfig(t *testing.T, fs afero.Fs) savedConfig {\n\tb, err := afero.ReadFile(fs, \".bk.yaml\")\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn savedConfig{}\n\t\t}\n\t\tt.Fatalf(\"failed to read config: %v\", err)\n\t}\n\n\tvar cfg savedConfig\n\tif len(b) == 0 {\n\t\treturn cfg\n\t}\n\tif err := yaml.Unmarshal(b, &cfg); err != nil {\n\t\tt.Fatalf(\"failed to unmarshal config: %v\", err)\n\t}\n\treturn cfg\n}\n\nfunc TestPickers(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"cached picker will save to local config\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\n\t\tpipelines := []pipeline.Pipeline{\n\t\t\t{Name: \"pipeline\", Org: \"org\"},\n\t\t}\n\t\tpicked := resolver.CachedPicker(conf, resolver.PassthruPicker)(pipelines)\n\n\t\tif picked == nil {\n\t\t\tt.Fatal(\"Should not have received nil from picker\")\n\t\t}\n\n\t\tsaved := readSavedConfig(t, fs)\n\t\tif len(saved.Pipelines) != 1 || saved.Pipelines[0] != \"pipeline\" {\n\t\t\tt.Fatalf(\"Local config pipelines do not match expected: %#v\", saved.Pipelines)\n\t\t}\n\t})\n\n\tt.Run(\"cached picker doesnt save if user makes no choice\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\n\t\tpipelines := []pipeline.Pipeline{}\n\t\tresolver.CachedPicker(conf, func(p []pipeline.Pipeline) *pipeline.Pipeline { return nil })(pipelines)\n\n\t\tb, _ := afero.ReadFile(fs, \".bk.yaml\")\n\t\texpected := \"\"\n\t\tif string(b) != expected {\n\t\t\tt.Fatalf(\"Local config file does not match expected: %s\", string(b))\n\t\t}\n\t})\n\n\tt.Run(\"cached picker saves correct pipeline first\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\n\t\tpipelines := []pipeline.Pipeline{\n\t\t\t{Name: \"first\"},\n\t\t\t{Name: \"second\"},\n\t\t\t{Name: \"third\"},\n\t\t}\n\t\tresolver.CachedPicker(conf, func(p []pipeline.Pipeline) *pipeline.Pipeline { return &p[1] })(pipelines)\n\n\t\tsaved := readSavedConfig(t, fs)\n\t\texpected := []string{\"second\", \"first\", \"third\"}\n\t\tif len(saved.Pipelines) != len(expected) {\n\t\t\tt.Fatalf(\"Local config pipelines length mismatch: got %d want %d\", len(saved.Pipelines), len(expected))\n\t\t}\n\t\tfor i, name := range expected {\n\t\t\tif saved.Pipelines[i] != name {\n\t\t\t\tt.Fatalf(\"Local config pipelines mismatch at %d: got %q want %q\", i, saved.Pipelines[i], name)\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/repository.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os/exec\"\n\t\"strings\"\n\n\tbkIO \"github.com/buildkite/cli/v3/internal/io\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\tgit \"github.com/go-git/go-git/v5\"\n)\n\n// ResolveFromRepository finds pipelines based on the current repository.\n//\n// It queries the API for all pipelines in the organization that match the repository's URL.\n// It delegates picking one from the list of matches to the `picker`.\nfunc ResolveFromRepository(f *factory.Factory, picker PipelinePicker) PipelineResolverFn {\n\treturn resolveFromRepositoryWithOrg(f, picker, f.Config.OrganizationSlug())\n}\n\n// ResolveFromRepositoryInOrg finds pipelines in a specific organization based\n// on the current repository.\nfunc ResolveFromRepositoryInOrg(f *factory.Factory, picker PipelinePicker, org string) PipelineResolverFn {\n\treturn resolveFromRepositoryWithOrg(f, picker, org)\n}\n\nfunc resolveFromRepositoryWithOrg(f *factory.Factory, picker PipelinePicker, org string) PipelineResolverFn {\n\treturn func(ctx context.Context) (*pipeline.Pipeline, error) {\n\t\tvar pipelines []pipeline.Pipeline\n\t\tif err := bkIO.SpinWhile(f, \"Resolving pipeline\", func() error {\n\t\t\tvar apiErr error\n\t\t\tpipelines, apiErr = resolveFromRepository(ctx, f, org)\n\t\t\treturn apiErr\n\t\t}); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(pipelines) == 0 {\n\t\t\treturn nil, nil\n\t\t}\n\t\tpipeline := picker(pipelines)\n\t\tif pipeline == nil {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\treturn pipeline, nil\n\t}\n}\n\nfunc resolveFromRepository(ctx context.Context, f *factory.Factory, org string) ([]pipeline.Pipeline, error) {\n\trepos, err := getRepoURLs(f.GitRepository)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(repos) == 0 {\n\t\trepos, err = getRepoURLsFromGit(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn filterPipelines(ctx, repos, org, f.RestAPIClient)\n}\n\nfunc filterPipelines(ctx context.Context, repoURLs []string, org string, client *buildkite.Client) ([]pipeline.Pipeline, error) {\n\tvar currentPipelines []pipeline.Pipeline\n\tpage := 1\n\tper_page := 30\n\tfor _, repoURL := range repoURLs {\n\t\tfor more_pipelines := true; more_pipelines; {\n\t\t\topts := buildkite.PipelineListOptions{\n\t\t\t\tListOptions: buildkite.ListOptions{\n\t\t\t\t\tPage:    page,\n\t\t\t\t\tPerPage: per_page,\n\t\t\t\t},\n\t\t\t\tRepository: repoURL,\n\t\t\t}\n\n\t\t\tpipelines, resp, err := client.Pipelines.List(ctx, org, &opts)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tfor _, p := range pipelines {\n\t\t\t\tfor _, u := range repoURLs {\n\t\t\t\t\tgitUrl := u[strings.LastIndex(u, \"/\")+1:]\n\t\t\t\t\tif strings.Contains(p.Repository, gitUrl) {\n\t\t\t\t\t\tcurrentPipelines = append(currentPipelines, pipeline.Pipeline{Name: p.Slug, Org: org})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif resp.NextPage == 0 {\n\t\t\t\tmore_pipelines = false\n\t\t\t} else {\n\t\t\t\tpage = resp.NextPage\n\t\t\t}\n\t\t}\n\t}\n\treturn currentPipelines, nil\n}\n\nfunc getRepoURLs(r *git.Repository) ([]string, error) {\n\tif r == nil {\n\t\treturn nil, nil // could not resolve to any repository, proceed to another resolver\n\t}\n\n\tc, err := r.Config()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif _, ok := c.Remotes[\"origin\"]; !ok {\n\t\treturn nil, nil // repo's \"origin\" remote does not exist, proceed to another resolver\n\t}\n\treturn c.Remotes[\"origin\"].URLs, nil\n}\n\nfunc getRepoURLsFromGit(ctx context.Context) ([]string, error) {\n\tcmd := exec.CommandContext(ctx, \"git\", \"remote\", \"get-url\", \"--all\", \"origin\")\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\tvar exitErr *exec.ExitError\n\t\tvar execErr *exec.Error\n\t\tif errors.As(err, &exitErr) || errors.As(err, &execErr) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tvar urls []string\n\tfor _, line := range strings.Split(string(output), \"\\n\") {\n\t\turl := strings.TrimSpace(line)\n\t\tif url == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\turls = append(urls, url)\n\t}\n\n\treturn urls, nil\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/repository_test.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/buildkite/cli/v3/pkg/cmd/factory\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\tgit \"github.com/go-git/go-git/v5\"\n\tgitconfig \"github.com/go-git/go-git/v5/config\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc TestResolvePipelinesFromPath(t *testing.T) {\n\tt.Parallel()\n\tctx := context.Background()\n\tconst testOrg = \"testOrg\"\n\n\tt.Run(\"no pipelines found\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// mock a response that doesn't match the current repository url\n\t\ts := mockHTTPServer(`[{\"slug\": \"my-pipeline\", \"repository\": \"git@github.com:buildkite/test.git\"}]`)\n\t\tt.Cleanup(s.Close)\n\n\t\tf := testFactory(t, s.URL, testOrg, testRepository(t, \"https://github.com/buildkite/cli.git\"))\n\t\tpipelines, err := resolveFromRepository(ctx, f, testOrg)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Error: %s\", err)\n\t\t}\n\t\tif len(pipelines) != 0 {\n\t\t\tt.Errorf(\"Expected 0 pipeline, got %d\", len(pipelines))\n\t\t}\n\t})\n\n\tt.Run(\"one pipeline\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// mock an http client response with a single pipeline matching the current repo url\n\t\ts := mockHTTPServer(`[{\"slug\": \"my-pipeline\", \"repository\": \"git@github.com:buildkite/cli.git\"}]`)\n\t\tt.Cleanup(s.Close)\n\n\t\tf := testFactory(t, s.URL, testOrg, testRepository(t, \"https://github.com/buildkite/cli.git\"))\n\t\tpipelines, err := resolveFromRepository(ctx, f, testOrg)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Error: %s\", err)\n\t\t}\n\t\tif len(pipelines) != 1 {\n\t\t\tt.Errorf(\"Expected 1 pipeline, got %d\", len(pipelines))\n\t\t}\n\t})\n\n\tt.Run(\"multiple pipelines\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\t// mock an http client response with 2 pipelines matching the current repo url\n\t\ts := mockHTTPServer(`[{\"slug\": \"my-pipeline\", \"repository\": \"git@github.com:buildkite/cli.git\"}, {\"slug\": \"my-pipeline-2\", \"repository\": \"git@github.com:buildkite/cli.git\"}]`)\n\t\tt.Cleanup(s.Close)\n\n\t\tf := testFactory(t, s.URL, testOrg, testRepository(t, \"https://github.com/buildkite/cli.git\"))\n\t\tpipelines, err := resolveFromRepository(ctx, f, testOrg)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Error: %s\", err)\n\t\t}\n\t\tif len(pipelines) != 2 {\n\t\t\tt.Errorf(\"Expected 2 pipeline, got %d\", len(pipelines))\n\t\t}\n\t})\n\n\tt.Run(\"no repository found\", func(t *testing.T) {\n\t\ts := mockHTTPServer(`[{\"slug\": \"\", \"repository\": \"\"}]`)\n\t\tt.Cleanup(s.Close)\n\n\t\tf := testFactory(t, s.URL, testOrg, nil)\n\t\tpipelines, err := resolveFromRepository(ctx, f, testOrg)\n\t\tif pipelines != nil {\n\t\t\tt.Errorf(\"Expected nil, got %v\", pipelines)\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Expected nil, got error: %s\", err)\n\t\t}\n\t})\n\n\tt.Run(\"no remote repository found\", func(t *testing.T) {\n\t\ts := mockHTTPServer(`[{\"slug\": \"\", \"repository\": \"\"}]`)\n\t\tt.Cleanup(s.Close)\n\n\t\tf := testFactory(t, s.URL, testOrg, testRepository(t))\n\t\tpipelines, err := resolveFromRepository(ctx, f, testOrg)\n\t\tif pipelines != nil {\n\t\t\tt.Errorf(\"Expected nil, got %v\", pipelines)\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Expected nil, got error: %s\", err)\n\t\t}\n\t})\n}\n\nfunc TestResolvePipelinesFromGitFallback(t *testing.T) {\n\tctx := context.Background()\n\tconst testOrg = \"testOrg\"\n\n\ts := mockHTTPServer(`[{\"slug\": \"cli-resolver-smoke\", \"repository\": \"git@github.com:buildkite/cli.git\"}]`)\n\tt.Cleanup(s.Close)\n\n\trepo := testRepository(t, \"https://github.com/buildkite/cli.git\")\n\twt, err := repo.Worktree()\n\tif err != nil {\n\t\tt.Fatalf(\"Worktree returned error: %v\", err)\n\t}\n\tt.Chdir(wt.Filesystem.Root())\n\n\tf := testFactory(t, s.URL, testOrg, nil)\n\tpipelines, err := resolveFromRepository(ctx, f, testOrg)\n\tif err != nil {\n\t\tt.Errorf(\"Error: %s\", err)\n\t}\n\tif len(pipelines) != 1 {\n\t\tt.Errorf(\"Expected 1 pipeline, got %d\", len(pipelines))\n\t}\n\tif len(pipelines) == 1 && pipelines[0].Name != \"cli-resolver-smoke\" {\n\t\tt.Errorf(\"Expected cli-resolver-smoke pipeline, got %s\", pipelines[0].Name)\n\t}\n}\n\nfunc testRepository(t *testing.T, remoteURLs ...string) *git.Repository {\n\tt.Helper()\n\n\trepo, err := git.PlainInit(t.TempDir(), false)\n\tif err != nil {\n\t\tt.Fatalf(\"PlainInit returned error: %v\", err)\n\t}\n\tif len(remoteURLs) == 0 {\n\t\treturn repo\n\t}\n\n\t_, err = repo.CreateRemote(&gitconfig.RemoteConfig{Name: \"origin\", URLs: remoteURLs})\n\tif err != nil {\n\t\tt.Fatalf(\"CreateRemote returned error: %v\", err)\n\t}\n\n\treturn repo\n}\n\nfunc testFactory(t *testing.T, serverURL string, org string, repo *git.Repository) *factory.Factory {\n\tt.Helper()\n\n\tbkClient, err := buildkite.NewOpts(buildkite.WithBaseURL(serverURL))\n\tif err != nil {\n\t\tt.Errorf(\"Error creating buildkite client: %s\", err)\n\t}\n\n\tconf := config.New(afero.NewMemMapFs(), nil)\n\tconf.SelectOrganization(org, true)\n\treturn &factory.Factory{\n\t\tConfig:        conf,\n\t\tRestAPIClient: bkClient,\n\t\tGitRepository: repo,\n\t}\n}\n\nfunc mockHTTPServer(response string) *httptest.Server {\n\treturn httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write([]byte(response))\n\t}))\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/resolver.go",
    "content": "package resolver\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n)\n\n// PipelineResolverFn is a function for the purpose of finding a pipeline. It returns an error if an irrecoverable\n// scenario happens and should halt execution. Otherwise if the resolver does not find a pipeline, it should return\n// (nil, nil) to indicate this. ie. no error occurred, but no pipeline was found either.\ntype PipelineResolverFn func(context.Context) (*pipeline.Pipeline, error)\n\ntype AggregateResolver []PipelineResolverFn\n\n// Resolve is a PipelineResolverFn that wraps up a list of resolvers to loop through to try find a pipeline. The first\n// pipeline that is found will be returned, if none are found if won't return an error to match the expectation of a\n// PipelineResolveFn\n//\n// This is safe to call multiple times. The same result will be returned.\nfunc (pr AggregateResolver) Resolve(ctx context.Context) (*pipeline.Pipeline, error) {\n\tfor _, resolve := range pr {\n\t\tp, err := resolve(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif p != nil {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\n// NewAggregateResolver creates an AggregregateResolver from a list of PipelineResolverFn, appending a final resolver\n// for capturing the case that no resolvers find a pipeline\nfunc NewAggregateResolver(resolvers ...PipelineResolverFn) AggregateResolver {\n\t// add a final error resolver to the chain in case no other resolvers find a pipeline\n\treturn append(resolvers, errorResolver)\n}\n\nfunc errorResolver(context.Context) (*pipeline.Pipeline, error) {\n\treturn nil, errors.New(\"failed to resolve a pipeline\")\n}\n\n// WithOrg wraps a resolver and forces any resolved pipeline to use the\n// provided organization.\nfunc WithOrg(org string, resolve PipelineResolverFn) PipelineResolverFn {\n\tif org == \"\" {\n\t\treturn resolve\n\t}\n\n\treturn func(ctx context.Context) (*pipeline.Pipeline, error) {\n\t\tp, err := resolve(ctx)\n\t\tif err != nil || p == nil {\n\t\t\treturn p, err\n\t\t}\n\n\t\tp.Org = org\n\t\treturn p, nil\n\t}\n}\n"
  },
  {
    "path": "internal/pipeline/resolver/resolver_test.go",
    "content": "package resolver_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/pipeline\"\n\t\"github.com/buildkite/cli/v3/internal/pipeline/resolver\"\n)\n\nfunc TestAggregateResolver(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"it loops over resolvers until one returns\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tagg := resolver.AggregateResolver{\n\t\t\tfunc(context.Context) (*pipeline.Pipeline, error) { return nil, nil },\n\t\t\tfunc(context.Context) (*pipeline.Pipeline, error) { return &pipeline.Pipeline{Name: \"test\"}, nil },\n\t\t}\n\n\t\tp, err := agg.Resolve(context.Background())\n\n\t\tif p.Name != \"test\" {\n\t\t\tt.Fatalf(\"Resolve function did not return expected value: %s\", p.Name)\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Fatal(\"Resolve returned an error\")\n\t\t}\n\t})\n\n\tt.Run(\"returns nil if nothing resolves\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tagg := resolver.AggregateResolver{}\n\n\t\tp, err := agg.Resolve(context.Background())\n\n\t\tif p != nil && err != nil {\n\t\t\tt.Fatal(\"Resolve did not return nil\")\n\t\t}\n\t})\n}\n\nfunc TestWithOrg(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"returns original resolver when org is empty\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tresolve := resolver.WithOrg(\"\", func(context.Context) (*pipeline.Pipeline, error) {\n\t\t\treturn &pipeline.Pipeline{Org: \"config-org\", Name: \"pipeline\"}, nil\n\t\t})\n\n\t\tp, err := resolve(context.Background())\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif p.Org != \"config-org\" {\n\t\t\tt.Fatalf(\"expected org config-org, got %s\", p.Org)\n\t\t}\n\t})\n\n\tt.Run(\"overrides resolved organization\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tresolve := resolver.WithOrg(\"override-org\", func(context.Context) (*pipeline.Pipeline, error) {\n\t\t\treturn &pipeline.Pipeline{Org: \"config-org\", Name: \"pipeline\"}, nil\n\t\t})\n\n\t\tp, err := resolve(context.Background())\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t}\n\t\tif p.Org != \"override-org\" {\n\t\t\tt.Fatalf(\"expected org override-org, got %s\", p.Org)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/preflight/branch_build.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\tbuildstate \"github.com/buildkite/cli/v3/internal/build/state\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// BranchBuild represents a preflight branch and its associated build status.\ntype BranchBuild struct {\n\tBranch string\n\tRef    string\n\tBuild  *buildkite.Build\n}\n\n// IsCompleted returns true if the associated build has reached a terminal state\n// (passed, failed, canceled, etc.), or if no build was found for the branch.\nfunc (bb BranchBuild) IsCompleted() bool {\n\tif bb.Build == nil {\n\t\treturn true\n\t}\n\treturn buildstate.IsTerminal(buildstate.State(bb.Build.State))\n}\n\n// ListRemotePreflightBranches returns all remote branches matching bk/preflight/*.\nfunc ListRemotePreflightBranches(dir string, debug bool) ([]BranchBuild, error) {\n\treturn lsRemotePreflightBranches(dir, \"refs/heads/bk/preflight/*\", debug)\n}\n\n// LookupRemotePreflightBranch returns the remote bk/preflight/<uuid> branch if it\n// exists, or nil if no such branch is present on the remote.\nfunc LookupRemotePreflightBranch(dir, uuid string, debug bool) (*BranchBuild, error) {\n\tbranches, err := lsRemotePreflightBranches(dir, \"refs/heads/bk/preflight/\"+uuid, debug)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(branches) == 0 {\n\t\treturn nil, nil\n\t}\n\treturn &branches[0], nil\n}\n\n// lsRemotePreflightBranches runs ls-remote against origin with the given ref\n// pattern and parses the results into BranchBuild entries.\nfunc lsRemotePreflightBranches(dir, pattern string, debug bool) ([]BranchBuild, error) {\n\tout, err := gitOutput(dir, nil, debug, \"ls-remote\", \"origin\", pattern)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"listing remote preflight branches: %w\", err)\n\t}\n\n\tif out == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tvar results []BranchBuild\n\tfor line := range strings.SplitSeq(out, \"\\n\") {\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tparts := strings.Fields(line)\n\t\tif len(parts) < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tref := parts[1]\n\t\tbranch := strings.TrimPrefix(ref, \"refs/heads/\")\n\t\tresults = append(results, BranchBuild{Branch: branch, Ref: ref})\n\t}\n\n\treturn results, nil\n}\n\n// maxResolveBuildPages is the maximum number of API pages to fetch when\n// resolving builds. This prevents runaway pagination when orphaned branches\n// have no matching builds.\nconst (\n\tmaxResolveBuildPages       = 10\n\tresolveBuildsPerPage       = 100\n\tmaxResolveBuildQueryLength = 6000\n)\n\n// ResolveBuilds looks up the most recent build for each preflight branch and\n// populates the Build field. Branches with no matching build retain a nil Build.\nfunc ResolveBuilds(ctx context.Context, client *buildkite.Client, org, pipeline string, branches []BranchBuild) error {\n\tif len(branches) == 0 {\n\t\treturn nil\n\t}\n\n\tresolved := make(map[string]*buildkite.Build, len(branches))\n\tfor _, branchBatch := range resolveBuildBranchBatches(branches) {\n\t\topts := &buildkite.BuildsListOptions{\n\t\t\tBranch:      branchBatch,\n\t\t\tListOptions: buildkite.ListOptions{PerPage: resolveBuildsPerPage},\n\t\t}\n\n\t\tfor page := 0; page < maxResolveBuildPages; page++ {\n\t\t\tbuilds, resp, err := client.Builds.ListByPipeline(ctx, org, pipeline, opts)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"listing builds for preflight branches: %w\", err)\n\t\t\t}\n\n\t\t\tfor i := range builds {\n\t\t\t\tif _, exists := resolved[builds[i].Branch]; !exists {\n\t\t\t\t\tresolved[builds[i].Branch] = &builds[i]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif len(builds) == 0 || len(resolved) >= len(branches) || resp.NextPage == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\topts.Page = resp.NextPage\n\t\t}\n\n\t\tif len(resolved) >= len(branches) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tfor i := range branches {\n\t\tbranches[i].Build = resolved[branches[i].Branch]\n\t}\n\n\treturn nil\n}\n\nfunc resolveBuildBranchBatches(branches []BranchBuild) [][]string {\n\tif len(branches) == 0 {\n\t\treturn nil\n\t}\n\n\tbatches := make([][]string, 0, 1)\n\tcurrent := make([]string, 0, len(branches))\n\tfor i := range branches {\n\t\tbranch := branches[i].Branch\n\t\tif len(current) > 0 && resolveBuildQueryLength(append(current, branch)) > maxResolveBuildQueryLength {\n\t\t\tbatches = append(batches, current)\n\t\t\tcurrent = []string{branch}\n\t\t\tcontinue\n\t\t}\n\t\tcurrent = append(current, branch)\n\t}\n\n\tif len(current) > 0 {\n\t\tbatches = append(batches, current)\n\t}\n\n\treturn batches\n}\n\nfunc resolveBuildQueryLength(branches []string) int {\n\tquery := url.Values{\n\t\t\"branch[]\": append([]string(nil), branches...),\n\t}\n\tquery.Set(\"per_page\", strconv.Itoa(resolveBuildsPerPage))\n\tquery.Set(\"page\", strconv.Itoa(maxResolveBuildPages))\n\treturn len(query.Encode())\n}\n"
  },
  {
    "path": "internal/preflight/branch_build_test.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestResolveBuilds_BatchesRequestsToAvoidLongQuery(t *testing.T) {\n\tconst maxRawQueryLen = 6500\n\n\tvar requestQueryLens []int\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif !strings.Contains(r.URL.Path, \"/builds\") {\n\t\t\thttp.NotFound(w, r)\n\t\t\treturn\n\t\t}\n\n\t\trequestQueryLens = append(requestQueryLens, len(r.URL.RawQuery))\n\t\tif len(r.URL.RawQuery) > maxRawQueryLen {\n\t\t\thttp.Error(w, `{\"message\":\"request uri too long\"}`, http.StatusRequestURITooLong)\n\t\t\treturn\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tbuilds := make([]buildkite.Build, 0, len(r.URL.Query()[\"branch[]\"]))\n\t\tfor _, branch := range r.URL.Query()[\"branch[]\"] {\n\t\t\tbuilds = append(builds, buildkite.Build{Branch: branch, State: \"passed\"})\n\t\t}\n\t\tif err := json.NewEncoder(w).Encode(builds); err != nil {\n\t\t\tt.Errorf(\"encoding response: %v\", err)\n\t\t}\n\t}))\n\tdefer server.Close()\n\n\tclient, err := buildkite.NewOpts(buildkite.WithBaseURL(server.URL))\n\tif err != nil {\n\t\tt.Fatalf(\"creating buildkite client: %v\", err)\n\t}\n\n\tbranches := make([]BranchBuild, 80)\n\tfor i := range branches {\n\t\tbranches[i] = BranchBuild{\n\t\t\tBranch: fmt.Sprintf(\"bk/preflight/%s-%02d\", strings.Repeat(\"x\", 96), i),\n\t\t}\n\t}\n\n\tif err := ResolveBuilds(context.Background(), client, \"test-org\", \"test-pipeline\", branches); err != nil {\n\t\tt.Fatalf(\"ResolveBuilds() error: %v\", err)\n\t}\n\n\tif len(requestQueryLens) < 2 {\n\t\tt.Fatalf(\"expected ResolveBuilds to batch requests, got %d request(s)\", len(requestQueryLens))\n\t}\n\n\tfor _, queryLen := range requestQueryLens {\n\t\tif queryLen > maxRawQueryLen {\n\t\t\tt.Fatalf(\"expected each request query to stay under %d bytes, got %d\", maxRawQueryLen, queryLen)\n\t\t}\n\t}\n\n\tfor i := range branches {\n\t\tif branches[i].Build == nil {\n\t\t\tt.Fatalf(\"expected branch %q to have a resolved build\", branches[i].Branch)\n\t\t}\n\t\tif branches[i].Build.Branch != branches[i].Branch {\n\t\t\tt.Fatalf(\"expected build for %q, got %q\", branches[i].Branch, branches[i].Build.Branch)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/preflight/cleanup.go",
    "content": "package preflight\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Cleanup deletes the preflight branch from the remote.\n// If the branch no longer exists on the remote, it is treated as success.\nfunc Cleanup(dir string, ref string, debug bool) error {\n\tout, err := gitOutput(dir, nil, debug, \"ls-remote\", \"origin\", ref)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif out == \"\" {\n\t\treturn nil\n\t}\n\n\trefspec := fmt.Sprintf(\":%s\", ref)\n\treturn gitRun(dir, nil, debug, \"push\", \"origin\", refspec)\n}\n\n// CleanupRefs deletes multiple refs from the remote in a single git push.\n// Refs that no longer exist on the remote are silently ignored.\nfunc CleanupRefs(dir string, refs []string, debug bool) error {\n\tif len(refs) == 0 {\n\t\treturn nil\n\t}\n\n\tout, err := gitOutput(dir, nil, debug, \"ls-remote\", \"origin\", \"refs/heads/bk/preflight/*\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tremote := make(map[string]struct{})\n\tfor line := range strings.SplitSeq(out, \"\\n\") {\n\t\tparts := strings.Fields(line)\n\t\tif len(parts) >= 2 {\n\t\t\tremote[parts[1]] = struct{}{}\n\t\t}\n\t}\n\n\targs := make([]string, 0, 2+len(refs))\n\targs = append(args, \"push\", \"origin\")\n\tfor _, ref := range refs {\n\t\tif _, exists := remote[ref]; exists {\n\t\t\targs = append(args, fmt.Sprintf(\":%s\", ref))\n\t\t}\n\t}\n\n\tif len(args) == 2 {\n\t\treturn nil\n\t}\n\n\treturn gitRun(dir, nil, debug, args...)\n}\n"
  },
  {
    "path": "internal/preflight/cleanup_test.go",
    "content": "package preflight\n\nimport (\n\t\"testing\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\t\"github.com/google/uuid\"\n)\n\nfunc TestBranchBuild_IsCompleted(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tbuild *buildkite.Build\n\t\twant  bool\n\t}{\n\t\t{\"nil build\", nil, true},\n\t\t{\"passed\", &buildkite.Build{State: \"passed\"}, true},\n\t\t{\"failed\", &buildkite.Build{State: \"failed\"}, true},\n\t\t{\"canceled\", &buildkite.Build{State: \"canceled\"}, true},\n\t\t{\"running\", &buildkite.Build{State: \"running\"}, false},\n\t\t{\"scheduled\", &buildkite.Build{State: \"scheduled\"}, false},\n\t\t{\"failing\", &buildkite.Build{State: \"failing\"}, false},\n\t\t{\"blocked\", &buildkite.Build{State: \"blocked\"}, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbb := BranchBuild{Branch: \"bk/preflight/test\", Build: tt.build}\n\t\t\tif got := bb.IsCompleted(); got != tt.want {\n\t\t\t\tt.Errorf(\"IsCompleted() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCleanup(t *testing.T) {\n\tworktree := initTestRepo(t)\n\n\tpreflightID := uuid.MustParse(\"00000000-0000-0000-0000-000000000010\")\n\tresult, err := Snapshot(worktree, preflightID)\n\tif err != nil {\n\t\tt.Fatalf(\"Snapshot() error: %v\", err)\n\t}\n\n\t// Verify the remote branch exists before cleanup.\n\tout := runGit(t, worktree, \"ls-remote\", \"origin\", result.Ref)\n\tif out == \"\" {\n\t\tt.Fatal(\"expected remote branch to exist before cleanup\")\n\t}\n\n\tif err := Cleanup(worktree, result.Ref, false); err != nil {\n\t\tt.Fatalf(\"Cleanup() error: %v\", err)\n\t}\n\n\t// Verify the remote branch no longer exists.\n\tout = runGit(t, worktree, \"ls-remote\", \"origin\", result.Ref)\n\tif out != \"\" {\n\t\tt.Errorf(\"expected remote branch to be deleted, got %q\", out)\n\t}\n}\n\nfunc TestCleanup_AlreadyDeleted(t *testing.T) {\n\tworktree := initTestRepo(t)\n\n\tpreflightID := uuid.MustParse(\"00000000-0000-0000-0000-000000000011\")\n\tresult, err := Snapshot(worktree, preflightID)\n\tif err != nil {\n\t\tt.Fatalf(\"Snapshot() error: %v\", err)\n\t}\n\n\t// Delete the branch manually first.\n\trunGit(t, worktree, \"push\", \"origin\", \"--delete\", result.Ref)\n\n\t// Cleanup should succeed even though the branch is already gone.\n\tif err := Cleanup(worktree, result.Ref, false); err != nil {\n\t\tt.Fatalf(\"Cleanup() should succeed when branch already deleted, got: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "internal/preflight/exit_policy.go",
    "content": "package preflight\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\n\tbkErrors \"github.com/buildkite/cli/v3/internal/errors\"\n)\n\ntype ExitPolicy int\n\nconst (\n\tExitOnBuildFailing ExitPolicy = iota\n\tExitOnBuildTerminal\n)\n\nfunc (p *ExitPolicy) UnmarshalText(text []byte) error {\n\tvalue := string(text)\n\tname, _, _ := strings.Cut(value, \":\")\n\tswitch name {\n\tcase \"build-failing\":\n\t\t*p = ExitOnBuildFailing\n\tcase \"build-terminal\":\n\t\t*p = ExitOnBuildTerminal\n\tdefault:\n\t\treturn bkErrors.NewValidationError(fmt.Errorf(\"unsupported --exit-on value %q\", value), \"invalid exit condition\")\n\t}\n\treturn nil\n}\n\nfunc EffectiveExitPolicy(policies []ExitPolicy) ExitPolicy {\n\tif slices.Contains(policies, ExitOnBuildTerminal) {\n\t\treturn ExitOnBuildTerminal\n\t}\n\treturn ExitOnBuildFailing\n}\n\nfunc ValidateExitPolicies(policies []ExitPolicy, watch bool) error {\n\tif len(policies) > 0 && !watch {\n\t\treturn bkErrors.NewValidationError(fmt.Errorf(\"--exit-on requires --watch\"), \"exit conditions require watch mode\")\n\t}\n\tif slices.Contains(policies, ExitOnBuildFailing) && slices.Contains(policies, ExitOnBuildTerminal) {\n\t\treturn bkErrors.NewValidationError(fmt.Errorf(\"build-failing and build-terminal cannot be used together\"), \"invalid exit conditions\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/preflight/git.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"slices\"\n\t\"strings\"\n)\n\n// gitCmdContext creates an exec.Command for git with the given dir and env pre-configured.\nfunc gitCmdContext(ctx context.Context, dir string, env []string, args ...string) *exec.Cmd {\n\tcmd := exec.CommandContext(ctx, \"git\", args...)\n\tcmd.Dir = dir\n\tcmd.Env = env\n\treturn cmd\n}\n\n// gitRun runs a git command, discarding output on success.\nfunc gitRun(dir string, env []string, debug bool, args ...string) error {\n\treturn gitRunContext(context.Background(), dir, env, debug, args...)\n}\n\n// gitRunContext runs a git command, discarding output on success.\nfunc gitRunContext(ctx context.Context, dir string, env []string, debug bool, args ...string) error {\n\tcmd := gitCmdContext(ctx, dir, env, args...)\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\tif debug {\n\t\t\tos.Stderr.Write(out)\n\t\t}\n\t\tif ctxErr := ctx.Err(); ctxErr != nil {\n\t\t\treturn fmt.Errorf(\"git %s: %w\", strings.Join(args, \" \"), ctxErr)\n\t\t}\n\t\treturn fmt.Errorf(\"git %s: %w\", strings.Join(args, \" \"), err)\n\t}\n\treturn nil\n}\n\n// gitOutput runs a git command and returns its trimmed stdout.\nfunc gitOutput(dir string, env []string, debug bool, args ...string) (string, error) {\n\treturn gitOutputContext(context.Background(), dir, env, debug, args...)\n}\n\n// gitOutputContext runs a git command and returns its trimmed stdout.\nfunc gitOutputContext(ctx context.Context, dir string, env []string, debug bool, args ...string) (string, error) {\n\tcmd := gitCmdContext(ctx, dir, env, args...)\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\tif debug {\n\t\t\tif ee, ok := err.(*exec.ExitError); ok {\n\t\t\t\tos.Stderr.Write(ee.Stderr)\n\t\t\t}\n\t\t}\n\t\tif ctxErr := ctx.Err(); ctxErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"git %s: %w\", strings.Join(args, \" \"), ctxErr)\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"git %s: %w\", strings.Join(args, \" \"), err)\n\t}\n\treturn strings.TrimSpace(string(out)), nil\n}\n\n// RepositoryRoot returns the top-level path for the git repository containing dir.\nfunc RepositoryRoot(dir string, debug bool) (string, error) {\n\treturn gitOutput(dir, nil, debug, \"rev-parse\", \"--show-toplevel\")\n}\n\n// SourceContext describes the original git state that preflight was created from.\ntype SourceContext struct {\n\tBranch string\n\tCommit string\n}\n\n// ResolveSourceContext returns the current branch name (if any) and HEAD commit.\nfunc ResolveSourceContext(dir string, debug bool) (SourceContext, error) {\n\tbranch, err := gitOutput(dir, nil, debug, \"branch\", \"--show-current\")\n\tif err != nil {\n\t\treturn SourceContext{}, err\n\t}\n\n\tcommit, err := gitOutput(dir, nil, debug, \"rev-parse\", \"HEAD\")\n\tif err != nil {\n\t\treturn SourceContext{}, err\n\t}\n\n\treturn SourceContext{Branch: branch, Commit: commit}, nil\n}\n\n// tempIndexEnv returns a copy of the current environment with GIT_INDEX_FILE\n// set to path, stripping any existing GIT_INDEX_FILE entry. This is used to\n// direct git commands at a temporary index without affecting the real one.\nfunc tempIndexEnv(path string) []string {\n\tenv := slices.DeleteFunc(os.Environ(), func(e string) bool {\n\t\treturn strings.HasPrefix(e, \"GIT_INDEX_FILE=\")\n\t})\n\treturn append(env, \"GIT_INDEX_FILE=\"+path)\n}\n"
  },
  {
    "path": "internal/preflight/run_summary.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\ntype SummaryOptions struct {\n\tIncludeFailures bool\n}\n\ntype SummaryResult struct {\n\tTests SummaryTests `json:\"tests\"`\n}\n\ntype SummaryTests struct {\n\tRuns     map[string]SummaryTestRun `json:\"runs\"`\n\tFailures []SummaryTestFailure      `json:\"failures\"`\n}\n\ntype SummaryTestRun struct {\n\tRunID     string `json:\"run_id\"`\n\tSuiteName string `json:\"suite_name,omitempty\"`\n\tSuiteSlug string `json:\"suite_slug\"`\n\tPassed    int    `json:\"passed\"`\n\tFailed    int    `json:\"failed\"`\n\tSkipped   int    `json:\"skipped\"`\n}\n\ntype SummaryTestFailure struct {\n\tRunID         string                 `json:\"run_id\"`\n\tSuiteName     string                 `json:\"suite_name,omitempty\"`\n\tSuiteSlug     string                 `json:\"suite_slug\"`\n\tName          string                 `json:\"name\"`\n\tLocation      string                 `json:\"location\"`\n\tMessage       string                 `json:\"message\"`\n\tFailureReason string                 `json:\"failure_reason\"`\n\tFailureDetail []SummaryFailureDetail `json:\"failure_detail\"`\n}\n\ntype SummaryFailureDetail struct {\n\tBacktrace []string `json:\"backtrace\"`\n\tExpanded  []string `json:\"expanded\"`\n}\n\ntype RunSummaryService struct {\n\tclient *buildkite.Client\n}\n\ntype RunSummaryGetOptions struct {\n\tResult          string\n\tIncludeFailures bool\n\tState           string\n}\n\ntype RunSummaryResponse struct {\n\tTests RunSummaryTests `json:\"tests\"`\n}\n\ntype RunSummaryTests struct {\n\tRuns     map[string]RunSummaryRun `json:\"runs\"`\n\tFailures []RunSummaryFailure      `json:\"failures\"`\n}\n\ntype RunSummaryRun struct {\n\tSuite   RunSummarySuite `json:\"suite\"`\n\tPassed  int             `json:\"passed\"`\n\tFailed  int             `json:\"failed\"`\n\tSkipped int             `json:\"skipped\"`\n}\n\ntype RunSummarySuite struct {\n\tID   string `json:\"id\"`\n\tSlug string `json:\"slug\"`\n\tName string `json:\"name\"`\n}\n\ntype RunSummaryFailure struct {\n\tRunID         string                `json:\"run_id\"`\n\tSuiteName     string                `json:\"suite_name\"`\n\tSuiteSlug     string                `json:\"suite_slug\"`\n\tName          string                `json:\"name\"`\n\tLocation      string                `json:\"location\"`\n\tFailureReason string                `json:\"failure_reason\"`\n\tLatestFail    *RunSummaryLatestFail `json:\"latest_fail,omitempty\"`\n}\n\ntype RunSummaryLatestFail struct {\n\tFailureReason   string                      `json:\"failure_reason\"`\n\tFailureExpanded []buildkite.FailureExpanded `json:\"failure_expanded,omitempty\"`\n}\n\nfunc NewRunSummaryService(client *buildkite.Client) *RunSummaryService {\n\treturn &RunSummaryService{client: client}\n}\n\nfunc (s *RunSummaryService) Get(ctx context.Context, org, buildID string, opt *RunSummaryGetOptions) (*RunSummaryResponse, error) {\n\tquery := url.Values{}\n\tif opt != nil {\n\t\tif opt.Result != \"\" {\n\t\t\tquery.Set(\"result\", opt.Result)\n\t\t}\n\t\tif opt.IncludeFailures {\n\t\t\tquery.Set(\"include\", \"latest_fail\")\n\t\t}\n\t\tif opt.State != \"\" {\n\t\t\tquery.Set(\"state\", opt.State)\n\t\t}\n\t}\n\n\tu := fmt.Sprintf(\"v2/analytics/organizations/%s/builds/%s/preflight/v1\", org, buildID)\n\tif encoded := query.Encode(); encoded != \"\" {\n\t\tu += \"?\" + encoded\n\t}\n\n\treq, err := s.client.NewRequest(ctx, \"GET\", u, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar summary RunSummaryResponse\n\t_, err = s.client.Do(req, &summary)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &summary, nil\n}\n\nfunc (r RunSummaryResponse) SummaryResult() SummaryResult {\n\ttests := make(map[string]SummaryTestRun, len(r.Tests.Runs))\n\n\tfor runID, run := range r.Tests.Runs {\n\t\ttests[runID] = SummaryTestRun{\n\t\t\tRunID:     runID,\n\t\t\tSuiteName: strings.TrimSpace(run.Suite.Name),\n\t\t\tSuiteSlug: strings.TrimSpace(run.Suite.Slug),\n\t\t\tPassed:    run.Passed,\n\t\t\tFailed:    run.Failed,\n\t\t\tSkipped:   run.Skipped,\n\t\t}\n\t}\n\n\tfailures := make([]SummaryTestFailure, 0, len(r.Tests.Failures))\n\tfor _, failure := range r.Tests.Failures {\n\t\tfailures = append(failures, failure.summaryFailure())\n\t}\n\n\treturn SummaryResult{Tests: SummaryTests{Runs: tests, Failures: failures}}\n}\n\nfunc (f RunSummaryFailure) summaryFailure() SummaryTestFailure {\n\tresult := SummaryTestFailure{\n\t\tRunID:         strings.TrimSpace(f.RunID),\n\t\tSuiteName:     strings.TrimSpace(f.SuiteName),\n\t\tSuiteSlug:     strings.TrimSpace(f.SuiteSlug),\n\t\tName:          strings.TrimSpace(f.Name),\n\t\tLocation:      f.Location,\n\t\tFailureReason: f.FailureReason,\n\t\tFailureDetail: []SummaryFailureDetail{},\n\t}\n\n\tif f.LatestFail == nil {\n\t\tresult.Message = f.FailureReason\n\t\treturn result\n\t}\n\n\tresult.Message = f.LatestFail.FailureReason\n\tif result.FailureReason == \"\" {\n\t\tresult.FailureReason = f.LatestFail.FailureReason\n\t}\n\n\tfor _, detail := range f.LatestFail.FailureExpanded {\n\t\tresult.FailureDetail = append(result.FailureDetail, SummaryFailureDetail{\n\t\t\tBacktrace: detail.Backtrace,\n\t\t\tExpanded:  detail.Expanded,\n\t\t})\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "internal/preflight/run_summary_test.go",
    "content": "package preflight\n\nimport \"testing\"\n\nfunc TestRunSummaryResponse_SummaryResult_PreservesRunsByRunID(t *testing.T) {\n\tt.Parallel()\n\n\tresult := RunSummaryResponse{\n\t\tTests: RunSummaryTests{\n\t\t\tRuns: map[string]RunSummaryRun{\n\t\t\t\t\"run-1\": {Suite: RunSummarySuite{Name: \"RSpec\", Slug: \"rspec\"}, Passed: 10, Failed: 1, Skipped: 2},\n\t\t\t\t\"run-2\": {Suite: RunSummarySuite{Name: \"RSpec\", Slug: \"rspec\"}, Passed: 12, Failed: 0, Skipped: 1},\n\t\t\t},\n\t\t\tFailures: []RunSummaryFailure{{\n\t\t\t\tRunID:         \"run-1\",\n\t\t\t\tSuiteName:     \"RSpec\",\n\t\t\t\tSuiteSlug:     \"rspec\",\n\t\t\t\tName:          \"example spec\",\n\t\t\t\tFailureReason: \"boom\",\n\t\t\t}},\n\t\t},\n\t}.SummaryResult()\n\n\tif len(result.Tests.Runs) != 2 {\n\t\tt.Fatalf(\"expected 2 runs, got %d\", len(result.Tests.Runs))\n\t}\n\n\trun1, ok := result.Tests.Runs[\"run-1\"]\n\tif !ok {\n\t\tt.Fatal(\"expected run-1 summary\")\n\t}\n\tif run1.RunID != \"run-1\" || run1.SuiteName != \"RSpec\" || run1.SuiteSlug != \"rspec\" || run1.Passed != 10 || run1.Failed != 1 || run1.Skipped != 2 {\n\t\tt.Fatalf(\"unexpected run-1 summary: %+v\", run1)\n\t}\n\n\trun2, ok := result.Tests.Runs[\"run-2\"]\n\tif !ok {\n\t\tt.Fatal(\"expected run-2 summary\")\n\t}\n\tif run2.RunID != \"run-2\" || run2.SuiteName != \"RSpec\" || run2.SuiteSlug != \"rspec\" || run2.Passed != 12 || run2.Failed != 0 || run2.Skipped != 1 {\n\t\tt.Fatalf(\"unexpected run-2 summary: %+v\", run2)\n\t}\n\n\tif len(result.Tests.Failures) != 1 {\n\t\tt.Fatalf(\"expected 1 failure, got %d\", len(result.Tests.Failures))\n\t}\n\tif result.Tests.Failures[0].RunID != \"run-1\" || result.Tests.Failures[0].SuiteName != \"RSpec\" {\n\t\tt.Fatalf(\"expected failure run_id to be preserved, got %+v\", result.Tests.Failures[0])\n\t}\n}\n"
  },
  {
    "path": "internal/preflight/snapshot.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/google/uuid\"\n)\n\n// FileChange represents a single file changed in the snapshot.\ntype FileChange struct {\n\tStatus string // e.g. \"M\", \"A\", \"D\", \"R\"\n\tPath   string\n}\n\n// SnapshotResult holds the output of a successful snapshot operation.\ntype SnapshotResult struct {\n\tCommit string\n\tRef    string\n\tBranch string\n\tFiles  []FileChange\n}\n\nfunc (r SnapshotResult) ShortCommit() string {\n\tif len(r.Commit) >= 10 {\n\t\treturn r.Commit[:10]\n\t}\n\treturn r.Commit\n}\n\n// StatusSymbol returns a human-readable symbol for the file change status.\nfunc (f FileChange) StatusSymbol() string {\n\tswitch f.Status {\n\tcase \"A\":\n\t\treturn \"+\"\n\tcase \"D\":\n\t\treturn \"-\"\n\tdefault:\n\t\treturn \"~\"\n\t}\n}\n\ntype snapshotConfig struct {\n\tdebug bool\n}\n\n// SnapshotOption configures Snapshot behavior.\ntype SnapshotOption func(*snapshotConfig)\n\n// WithDebug enables verbose git output on failure.\nfunc WithDebug() SnapshotOption {\n\treturn func(cfg *snapshotConfig) { cfg.debug = true }\n}\n\n// Snapshot pushes the current working tree state to a remote preflight ref.\n// It always creates a distinct commit on top of HEAD (even when the worktree\n// is clean) without touching the real git index.\nfunc Snapshot(dir string, preflightID uuid.UUID, opts ...SnapshotOption) (*SnapshotResult, error) {\n\treturn SnapshotContext(context.Background(), dir, preflightID, opts...)\n}\n\n// SnapshotContext pushes the current working tree state to a remote preflight\n// ref, aborting in-flight git commands when ctx is canceled.\nfunc SnapshotContext(ctx context.Context, dir string, preflightID uuid.UUID, opts ...SnapshotOption) (*SnapshotResult, error) {\n\tcfg := &snapshotConfig{}\n\tfor _, opt := range opts {\n\t\topt(cfg)\n\t}\n\ttmp, err := os.CreateTemp(\"\", \"git-index-*\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"create temp index: %w\", err)\n\t}\n\ttmpIndex := tmp.Name()\n\ttmp.Close()\n\tdefer os.Remove(tmpIndex)\n\n\tenv := tempIndexEnv(tmpIndex)\n\n\t// Seed the temp index from HEAD.\n\tif err := gitRunContext(ctx, dir, env, cfg.debug, \"read-tree\", \"HEAD\"); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Stage the entire worktree into the temp index.\n\tif err := gitRunContext(ctx, dir, env, cfg.debug, \"add\", \"-A\"); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Diff the temp index against HEAD to find changed files.\n\tfiles, err := diffFilesContext(ctx, dir, env, cfg.debug)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thead, err := gitOutputContext(ctx, dir, env, cfg.debug, \"rev-parse\", \"HEAD\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbranch := fmt.Sprintf(\"bk/preflight/%s\", preflightID.String())\n\tref := fmt.Sprintf(\"refs/heads/%s\", branch)\n\n\t// Always write a tree and create a new commit, even when there are no\n\t// local changes. This ensures the preflight branch always points to a\n\t// distinct commit (not shared with HEAD), which allows commit statuses to\n\t// be attributed to the preflight run rather than the base commit.\n\ttree, err := gitOutputContext(ctx, dir, env, cfg.debug, \"write-tree\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmsg := fmt.Sprintf(\"Preflight snapshot\\n\\nPreflight Run ID: %s\\nBase Commit: %s\", preflightID, head)\n\tcommit, err := gitOutputContext(ctx, dir, env, cfg.debug, \"commit-tree\", tree, \"-p\", head, \"-m\", msg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Push the commit to the remote branch.\n\trefspec := fmt.Sprintf(\"%s:%s\", commit, ref)\n\tif err := gitRunContext(ctx, dir, env, cfg.debug, \"push\", \"origin\", refspec); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &SnapshotResult{\n\t\tCommit: commit,\n\t\tRef:    ref,\n\t\tBranch: branch,\n\t\tFiles:  files,\n\t}, nil\n}\n\n// diffFiles returns the list of files changed between HEAD and the temp index.\n// It uses -z for null-terminated output to correctly handle renames, copies,\n// and filenames containing spaces or special characters.\nfunc diffFiles(dir string, env []string, debug bool) ([]FileChange, error) {\n\treturn diffFilesContext(context.Background(), dir, env, debug)\n}\n\nfunc diffFilesContext(ctx context.Context, dir string, env []string, debug bool) ([]FileChange, error) {\n\tout, err := gitOutputContext(ctx, dir, env, debug, \"diff-index\", \"--cached\", \"--name-status\", \"-z\", \"-M\", \"HEAD\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif out == \"\" {\n\t\treturn nil, nil\n\t}\n\n\t// With -z, git outputs NUL-separated tokens:\n\t//   status \\0 path \\0           — for M, A, D, etc.\n\t//   status \\0 old_path \\0 new_path \\0  — for R (rename) and C (copy)\n\ttokens := strings.Split(out, \"\\x00\")\n\tvar files []FileChange\n\tfor i := 0; i < len(tokens); i++ {\n\t\tstatus := tokens[i]\n\t\tif status == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tcode := status[:1]\n\t\ti++\n\t\tif i >= len(tokens) {\n\t\t\tbreak\n\t\t}\n\n\t\tpath := tokens[i]\n\t\tif code == \"R\" || code == \"C\" {\n\t\t\t// Skip old path, use the new path.\n\t\t\ti++\n\t\t\tif i >= len(tokens) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tpath = tokens[i]\n\t\t}\n\n\t\tfiles = append(files, FileChange{\n\t\t\tStatus: code,\n\t\t\tPath:   path,\n\t\t})\n\t}\n\n\treturn files, nil\n}\n"
  },
  {
    "path": "internal/preflight/snapshot_test.go",
    "content": "package preflight\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n)\n\n// initTestRepo creates a real git repository in a temp directory with an\n// initial commit and a bare \"origin\" remote so that Snapshot can push.\n// It returns the worktree path and a cleanup-aware test helper.\nfunc initTestRepo(t *testing.T) string {\n\tt.Helper()\n\n\tdir := t.TempDir()\n\tworktree := filepath.Join(dir, \"work\")\n\tbare := filepath.Join(dir, \"origin.git\")\n\n\t// Create the bare remote.\n\trunGit(t, \"\", \"init\", \"--bare\", bare)\n\n\t// Create the working repo.\n\trunGit(t, \"\", \"init\", worktree)\n\trunGit(t, worktree, \"config\", \"user.email\", \"test@test.com\")\n\trunGit(t, worktree, \"config\", \"user.name\", \"Test\")\n\trunGit(t, worktree, \"config\", \"commit.gpgsign\", \"false\")\n\n\t// Create an initial commit so HEAD exists.\n\tinitial := filepath.Join(worktree, \"README.md\")\n\tif err := os.WriteFile(initial, []byte(\"# test\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\trunGit(t, worktree, \"add\", \".\")\n\trunGit(t, worktree, \"commit\", \"-m\", \"initial commit\")\n\n\t// Add the bare repo as origin.\n\trunGit(t, worktree, \"remote\", \"add\", \"origin\", bare)\n\n\treturn worktree\n}\n\nfunc runGit(t *testing.T, dir string, args ...string) string {\n\tt.Helper()\n\n\tcmd := exec.Command(\"git\", args...)\n\tif dir != \"\" {\n\t\tcmd.Dir = dir\n\t}\n\tout, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tt.Fatalf(\"git %s: %v\\n%s\", strings.Join(args, \" \"), err, out)\n\t}\n\treturn strings.TrimSpace(string(out))\n}\n\nfunc TestSnapshot_CommittedChanges(t *testing.T) {\n\tworktree := initTestRepo(t)\n\n\t// Add a tracked file change (but don't commit it).\n\tif err := os.WriteFile(filepath.Join(worktree, \"README.md\"), []byte(\"# updated\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tpreflightID := uuid.MustParse(\"00000000-0000-0000-0000-000000000001\")\n\tresult, err := Snapshot(worktree, preflightID)\n\tif err != nil {\n\t\tt.Fatalf(\"Snapshot() error: %v\", err)\n\t}\n\n\tif len(result.Commit) != 40 {\n\t\tt.Errorf(\"expected 40-char SHA, got %q (len %d)\", result.Commit, len(result.Commit))\n\t}\n\n\t// The commit should exist in the repo.\n\trunGit(t, worktree, \"cat-file\", \"-t\", result.Commit)\n\n\t// The snapshot tree should contain the updated content.\n\tcontent := runGit(t, worktree, \"show\", result.Commit+\":README.md\")\n\tif content != \"# updated\" {\n\t\tt.Errorf(\"snapshot content = %q, want %q\", content, \"# updated\")\n\t}\n\n\t// The remote branch should have been pushed.\n\tremoteCommit := runGit(t, worktree, \"ls-remote\", \"origin\", result.Ref)\n\tif !strings.Contains(remoteCommit, result.Commit) {\n\t\tt.Errorf(\"remote branch does not contain commit %s, got %q\", result.Commit, remoteCommit)\n\t}\n}\n\nfunc TestSnapshotContext_CancelsPush(t *testing.T) {\n\tworktree := initTestRepo(t)\n\n\tif err := os.WriteFile(filepath.Join(worktree, \"new-file.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tgitPath, err := exec.LookPath(\"git\")\n\tif err != nil {\n\t\tt.Fatalf(\"finding git: %v\", err)\n\t}\n\n\tfakeBin := t.TempDir()\n\tpushStarted := filepath.Join(fakeBin, \"push-started\")\n\tfakeGit := filepath.Join(fakeBin, \"git\")\n\tif err := os.WriteFile(fakeGit, []byte(`#!/bin/sh\nif [ \"$1\" = \"push\" ]; then\n\ttouch \"$PUSH_STARTED\"\n\texec /bin/sleep 10\nfi\nexec \"$REAL_GIT\" \"$@\"\n`), 0o755); err != nil {\n\t\tt.Fatalf(\"writing fake git: %v\", err)\n\t}\n\n\tt.Setenv(\"REAL_GIT\", gitPath)\n\tt.Setenv(\"PUSH_STARTED\", pushStarted)\n\tt.Setenv(\"PATH\", fakeBin+string(os.PathListSeparator)+os.Getenv(\"PATH\"))\n\n\tctx, cancel := context.WithCancel(context.Background())\n\terrCh := make(chan error, 1)\n\tgo func() {\n\t\t_, err := SnapshotContext(ctx, worktree, uuid.MustParse(\"00000000-0000-0000-0000-000000000020\"))\n\t\terrCh <- err\n\t}()\n\n\tdeadline := time.After(2 * time.Second)\n\tfor {\n\t\tif _, err := os.Stat(pushStarted); err == nil {\n\t\t\tbreak\n\t\t} else if !os.IsNotExist(err) {\n\t\t\tt.Fatalf(\"checking push marker: %v\", err)\n\t\t}\n\n\t\tselect {\n\t\tcase err := <-errCh:\n\t\t\tt.Fatalf(\"SnapshotContext returned before push was canceled: %v\", err)\n\t\tcase <-deadline:\n\t\t\tt.Fatal(\"timed out waiting for git push to start\")\n\t\tcase <-time.After(10 * time.Millisecond):\n\t\t}\n\t}\n\n\tcancel()\n\n\tselect {\n\tcase err := <-errCh:\n\t\tif !errors.Is(err, context.Canceled) {\n\t\t\tt.Fatalf(\"expected context canceled error, got: %v\", err)\n\t\t}\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatal(\"SnapshotContext did not return promptly after cancellation\")\n\t}\n}\n\nfunc TestSnapshot_UntrackedFiles(t *testing.T) {\n\tworktree := initTestRepo(t)\n\n\t// Add a brand new untracked file.\n\tif err := os.WriteFile(filepath.Join(worktree, \"new-file.txt\"), []byte(\"hello\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tpreflightID := uuid.MustParse(\"00000000-0000-0000-0000-000000000002\")\n\tresult, err := Snapshot(worktree, preflightID)\n\tif err != nil {\n\t\tt.Fatalf(\"Snapshot() error: %v\", err)\n\t}\n\n\t// The snapshot should include the untracked file.\n\tcontent := runGit(t, worktree, \"show\", result.Commit+\":new-file.txt\")\n\tif content != \"hello\" {\n\t\tt.Errorf(\"untracked file content = %q, want %q\", content, \"hello\")\n\t}\n}\n\nfunc TestSnapshot_DoesNotModifyRealIndex(t *testing.T) {\n\tworktree := initTestRepo(t)\n\n\t// Create an untracked file.\n\tif err := os.WriteFile(filepath.Join(worktree, \"untracked.txt\"), []byte(\"data\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Record the index state before snapshot.\n\tstatusBefore := runGit(t, worktree, \"status\", \"--porcelain\")\n\n\t_, err := Snapshot(worktree, uuid.MustParse(\"00000000-0000-0000-0000-000000000003\"))\n\tif err != nil {\n\t\tt.Fatalf(\"Snapshot() error: %v\", err)\n\t}\n\n\t// The real index should be unchanged.\n\tstatusAfter := runGit(t, worktree, \"status\", \"--porcelain\")\n\tif statusBefore != statusAfter {\n\t\tt.Errorf(\"git status changed after Snapshot:\\nbefore: %q\\nafter:  %q\", statusBefore, statusAfter)\n\t}\n}\n\nfunc TestSnapshot_UniquePreflightIDs(t *testing.T) {\n\tworktree := initTestRepo(t)\n\n\t// First snapshot.\n\tresult1, err := Snapshot(worktree, uuid.MustParse(\"00000000-0000-0000-0000-000000000004\"))\n\tif err != nil {\n\t\tt.Fatalf(\"first Snapshot() error: %v\", err)\n\t}\n\n\t// Modify a file and snapshot with a different preflight ID.\n\tif err := os.WriteFile(filepath.Join(worktree, \"README.md\"), []byte(\"# v2\\n\"), 0o644); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tresult2, err := Snapshot(worktree, uuid.MustParse(\"00000000-0000-0000-0000-000000000005\"))\n\tif err != nil {\n\t\tt.Fatalf(\"second Snapshot() error: %v\", err)\n\t}\n\n\tif result1.Commit == result2.Commit {\n\t\tt.Error(\"expected different commits for different snapshots\")\n\t}\n\n\t// Both remote branches should exist with their respective commits.\n\tremote1 := runGit(t, worktree, \"ls-remote\", \"origin\", result1.Ref)\n\tif !strings.Contains(remote1, result1.Commit) {\n\t\tt.Errorf(\"run-1 branch should point to %s, got %q\", result1.Commit, remote1)\n\t}\n\n\tremote2 := runGit(t, worktree, \"ls-remote\", \"origin\", result2.Ref)\n\tif !strings.Contains(remote2, result2.Commit) {\n\t\tt.Errorf(\"run-2 branch should point to %s, got %q\", result2.Commit, remote2)\n\t}\n}\n\nfunc TestSnapshotResult_ShortCommit(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tcommit string\n\t\twant   string\n\t}{\n\t\t{\n\t\t\tname:   \"full SHA is truncated to 10 chars\",\n\t\t\tcommit: \"abc123def456789000aabbccddeeff0011223344\",\n\t\t\twant:   \"abc123def4\",\n\t\t},\n\t\t{\n\t\t\tname:   \"exactly 10 chars\",\n\t\t\tcommit: \"abc123def4\",\n\t\t\twant:   \"abc123def4\",\n\t\t},\n\t\t{\n\t\t\tname:   \"short commit returned as-is\",\n\t\t\tcommit: \"abc\",\n\t\t\twant:   \"abc\",\n\t\t},\n\t\t{\n\t\t\tname:   \"empty commit\",\n\t\t\tcommit: \"\",\n\t\t\twant:   \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := SnapshotResult{Commit: tt.commit}\n\t\t\tif got := r.ShortCommit(); got != tt.want {\n\t\t\t\tt.Errorf(\"ShortCommit() = %q, want %q\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// setupDiffEnv creates a temp git index seeded from HEAD and returns the env\n// slice for use with diffFiles. The caller can stage changes into this index\n// using git commands with the returned env.\nfunc setupDiffEnv(t *testing.T, worktree string) []string {\n\tt.Helper()\n\n\ttmp, err := os.CreateTemp(\"\", \"git-index-test-*\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttmpIndex := tmp.Name()\n\ttmp.Close()\n\tt.Cleanup(func() { os.Remove(tmpIndex) })\n\n\tenv := append(os.Environ(), \"GIT_INDEX_FILE=\"+tmpIndex)\n\n\tcmd := exec.Command(\"git\", \"read-tree\", \"HEAD\")\n\tcmd.Dir = worktree\n\tcmd.Env = env\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\tt.Fatalf(\"git read-tree HEAD: %v\\n%s\", err, out)\n\t}\n\n\treturn env\n}\n\nfunc TestDiffFiles(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tsetup func(t *testing.T, worktree string, env []string)\n\t\twant  []FileChange\n\t}{\n\t\t{\n\t\t\tname: \"modified file\",\n\t\t\tsetup: func(t *testing.T, worktree string, env []string) {\n\t\t\t\tt.Helper()\n\t\t\t\tos.WriteFile(filepath.Join(worktree, \"README.md\"), []byte(\"# changed\\n\"), 0o644)\n\t\t\t\tcmd := exec.Command(\"git\", \"add\", \"README.md\")\n\t\t\t\tcmd.Dir = worktree\n\t\t\t\tcmd.Env = env\n\t\t\t\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\t\t\t\tt.Fatalf(\"git add: %v\\n%s\", err, out)\n\t\t\t\t}\n\t\t\t},\n\t\t\twant: []FileChange{{Status: \"M\", Path: \"README.md\"}},\n\t\t},\n\t\t{\n\t\t\tname: \"added file\",\n\t\t\tsetup: func(t *testing.T, worktree string, env []string) {\n\t\t\t\tt.Helper()\n\t\t\t\tos.WriteFile(filepath.Join(worktree, \"new.txt\"), []byte(\"new\\n\"), 0o644)\n\t\t\t\tcmd := exec.Command(\"git\", \"add\", \"new.txt\")\n\t\t\t\tcmd.Dir = worktree\n\t\t\t\tcmd.Env = env\n\t\t\t\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\t\t\t\tt.Fatalf(\"git add: %v\\n%s\", err, out)\n\t\t\t\t}\n\t\t\t},\n\t\t\twant: []FileChange{{Status: \"A\", Path: \"new.txt\"}},\n\t\t},\n\t\t{\n\t\t\tname: \"deleted file\",\n\t\t\tsetup: func(t *testing.T, worktree string, env []string) {\n\t\t\t\tt.Helper()\n\t\t\t\tos.Remove(filepath.Join(worktree, \"README.md\"))\n\t\t\t\tcmd := exec.Command(\"git\", \"add\", \"README.md\")\n\t\t\t\tcmd.Dir = worktree\n\t\t\t\tcmd.Env = env\n\t\t\t\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\t\t\t\tt.Fatalf(\"git add: %v\\n%s\", err, out)\n\t\t\t\t}\n\t\t\t},\n\t\t\twant: []FileChange{{Status: \"D\", Path: \"README.md\"}},\n\t\t},\n\t\t{\n\t\t\tname: \"renamed file\",\n\t\t\tsetup: func(t *testing.T, worktree string, env []string) {\n\t\t\t\tt.Helper()\n\t\t\t\tos.Rename(filepath.Join(worktree, \"README.md\"), filepath.Join(worktree, \"DOCS.md\"))\n\t\t\t\tcmd := exec.Command(\"git\", \"add\", \"-A\")\n\t\t\t\tcmd.Dir = worktree\n\t\t\t\tcmd.Env = env\n\t\t\t\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\t\t\t\tt.Fatalf(\"git add: %v\\n%s\", err, out)\n\t\t\t\t}\n\t\t\t},\n\t\t\twant: []FileChange{{Status: \"R\", Path: \"DOCS.md\"}},\n\t\t},\n\t\t{\n\t\t\tname: \"file with spaces in name\",\n\t\t\tsetup: func(t *testing.T, worktree string, env []string) {\n\t\t\t\tt.Helper()\n\t\t\t\tos.WriteFile(filepath.Join(worktree, \"my file.txt\"), []byte(\"data\\n\"), 0o644)\n\t\t\t\tcmd := exec.Command(\"git\", \"add\", \"my file.txt\")\n\t\t\t\tcmd.Dir = worktree\n\t\t\t\tcmd.Env = env\n\t\t\t\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\t\t\t\tt.Fatalf(\"git add: %v\\n%s\", err, out)\n\t\t\t\t}\n\t\t\t},\n\t\t\twant: []FileChange{{Status: \"A\", Path: \"my file.txt\"}},\n\t\t},\n\t\t{\n\t\t\tname: \"no changes\",\n\t\t\tsetup: func(t *testing.T, worktree string, env []string) {\n\t\t\t\tt.Helper()\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple changes\",\n\t\t\tsetup: func(t *testing.T, worktree string, env []string) {\n\t\t\t\tt.Helper()\n\t\t\t\tos.WriteFile(filepath.Join(worktree, \"README.md\"), []byte(\"# v2\\n\"), 0o644)\n\t\t\t\tos.WriteFile(filepath.Join(worktree, \"a.txt\"), []byte(\"a\\n\"), 0o644)\n\t\t\t\tos.WriteFile(filepath.Join(worktree, \"b.txt\"), []byte(\"b\\n\"), 0o644)\n\t\t\t\tcmd := exec.Command(\"git\", \"add\", \"-A\")\n\t\t\t\tcmd.Dir = worktree\n\t\t\t\tcmd.Env = env\n\t\t\t\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\t\t\t\tt.Fatalf(\"git add: %v\\n%s\", err, out)\n\t\t\t\t}\n\t\t\t},\n\t\t\twant: []FileChange{\n\t\t\t\t{Status: \"M\", Path: \"README.md\"},\n\t\t\t\t{Status: \"A\", Path: \"a.txt\"},\n\t\t\t\t{Status: \"A\", Path: \"b.txt\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tworktree := initTestRepo(t)\n\t\t\tenv := setupDiffEnv(t, worktree)\n\t\t\ttt.setup(t, worktree, env)\n\n\t\t\tgot, err := diffFiles(worktree, env, false)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"diffFiles() error: %v\", err)\n\t\t\t}\n\n\t\t\tif len(got) != len(tt.want) {\n\t\t\t\tt.Fatalf(\"diffFiles() returned %d files, want %d\\ngot: %+v\", len(got), len(tt.want), got)\n\t\t\t}\n\t\t\tfor i := range tt.want {\n\t\t\t\tif got[i].Status != tt.want[i].Status {\n\t\t\t\t\tt.Errorf(\"file[%d].Status = %q, want %q\", i, got[i].Status, tt.want[i].Status)\n\t\t\t\t}\n\t\t\t\tif got[i].Path != tt.want[i].Path {\n\t\t\t\t\tt.Errorf(\"file[%d].Path = %q, want %q\", i, got[i].Path, tt.want[i].Path)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSnapshot_CleanWorktree(t *testing.T) {\n\tworktree := initTestRepo(t)\n\n\tpreflightID := uuid.MustParse(\"00000000-0000-0000-0000-000000000006\")\n\tresult, err := Snapshot(worktree, preflightID)\n\tif err != nil {\n\t\tt.Fatalf(\"Snapshot() error: %v\", err)\n\t}\n\n\thead := runGit(t, worktree, \"rev-parse\", \"HEAD\")\n\n\t// Even with a clean worktree a new commit should always be created so\n\t// that commit statuses are attributed to the preflight run rather than\n\t// the shared HEAD commit.\n\tif result.Commit == head {\n\t\tt.Errorf(\"expected a new commit distinct from HEAD %s, but got the same SHA\", head)\n\t}\n\n\tif len(result.Files) != 0 {\n\t\tt.Errorf(\"expected no changed files, got %d\", len(result.Files))\n\t}\n\n\t// The new commit should be reachable and its parent should be HEAD.\n\tparent := runGit(t, worktree, \"rev-parse\", result.Commit+\"^\")\n\tif parent != head {\n\t\tt.Errorf(\"expected parent of snapshot commit to be HEAD %s, got %s\", head, parent)\n\t}\n\n\t// The remote branch should exist and point to the new commit.\n\tremoteRef := runGit(t, worktree, \"ls-remote\", \"origin\", result.Ref)\n\tif !strings.Contains(remoteRef, result.Commit) {\n\t\tt.Errorf(\"remote branch should point to snapshot commit %s, got %q\", result.Commit, remoteRef)\n\t}\n}\n"
  },
  {
    "path": "internal/secret/view.go",
    "content": "package secret\n\nimport (\n\t\"github.com/buildkite/cli/v3/pkg/output\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\n// SecretViewTable renders a table view of one or more cluster secrets\nfunc SecretViewTable(secrets ...buildkite.ClusterSecret) string {\n\tif len(secrets) == 0 {\n\t\treturn \"No secrets found.\"\n\t}\n\n\trows := make([][]string, 0, len(secrets))\n\tfor _, s := range secrets {\n\t\trows = append(rows, []string{\n\t\t\toutput.ValueOrDash(s.Key),\n\t\t\toutput.ValueOrDash(s.ID),\n\t\t\toutput.ValueOrDash(s.Description),\n\t\t})\n\t}\n\n\treturn output.Table(\n\t\t[]string{\"Key\", \"ID\", \"Description\"},\n\t\trows,\n\t\tmap[string]string{\"key\": \"bold\", \"id\": \"dim\", \"description\": \"dim\"},\n\t)\n}\n"
  },
  {
    "path": "internal/user/user.graphql",
    "content": "mutation InviteUser($organization: ID!, $emails: [String!]!) {\n  organizationInvitationCreate(\n    input: { organizationID: $organization, emails: $emails }\n  ) {\n    clientMutationId\n  }\n}\n\nquery FindUserByEmail($organization: ID!, $email: String!) {\n  organization(slug: $organization) {\n    members(first: 1, email: $email) {\n      edges {\n        node {\n          user {\n            id\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "internal/util/util.go",
    "content": "package util\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/pkg/browser\"\n)\n\nfunc GenerateGraphQLID(prefix, uuid string) string {\n\tvar graphqlID strings.Builder\n\twr := base64.NewEncoder(base64.StdEncoding, &graphqlID)\n\tfmt.Fprintf(wr, \"%s%s\", prefix, uuid)\n\twr.Close()\n\n\treturn graphqlID.String()\n}\n\nfunc OpenInWebBrowser(openInWeb bool, webUrl string) error {\n\tif openInWeb {\n\t\terr := browser.OpenURL(webUrl)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Error opening browser: %v\\n\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/validation/errors.go",
    "content": "package validation\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype ValidationErrors []ValidationError\n\ntype ValidationError struct {\n\tField   string\n\tMessage string\n}\n\nfunc (e *ValidationError) Error() string {\n\treturn fmt.Sprintf(\"%s: %s\", e.Field, e.Message)\n}\n\nfunc (e ValidationErrors) Error() string {\n\tif len(e) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar msgs []string\n\tfor _, err := range e {\n\t\tmsgs = append(msgs, err.Error())\n\t}\n\treturn strings.Join(msgs, \"\\n\")\n}\n"
  },
  {
    "path": "internal/validation/rule.go",
    "content": "package validation\n\ntype Rule interface {\n\tValidate(value interface{}) error\n}\n"
  },
  {
    "path": "internal/validation/validator.go",
    "content": "package validation\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n)\n\ntype Validator struct {\n\trules map[string][]Rule\n}\n\nfunc New() *Validator {\n\treturn &Validator{\n\t\trules: make(map[string][]Rule),\n\t}\n}\n\nfunc (v *Validator) AddRule(field string, rule Rule) {\n\tv.rules[field] = append(v.rules[field], rule)\n}\n\n// Validate validates a map of field/value pairs\nfunc (v *Validator) Validate(fields map[string]interface{}) error {\n\tvar errors ValidationErrors\n\n\tfor field, value := range fields {\n\t\tif rules, ok := v.rules[field]; ok {\n\t\t\tfor _, rule := range rules {\n\t\t\t\tif err := rule.Validate(value); err != nil {\n\t\t\t\t\terrors = append(errors, ValidationError{\n\t\t\t\t\t\tField:   field,\n\t\t\t\t\t\tMessage: err.Error(),\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(errors) > 0 {\n\t\treturn errors\n\t}\n\treturn nil\n}\n\ntype RequiredRule struct{}\n\nfunc (r RequiredRule) Validate(value interface{}) error {\n\tif value == nil {\n\t\treturn fmt.Errorf(\"field is required\")\n\t}\n\tif s, ok := value.(string); ok && strings.TrimSpace(s) == \"\" {\n\t\treturn fmt.Errorf(\"field is required\")\n\t}\n\treturn nil\n}\n\ntype SlugRule struct{}\n\nfunc (r SlugRule) Validate(value interface{}) error {\n\ts, ok := value.(string)\n\tif !ok {\n\t\treturn fmt.Errorf(\"value must be a string\")\n\t}\n\n\tmatched, _ := regexp.MatchString(`^[a-zA-Z0-9]+[a-zA-Z0-9-]*$`, s)\n\tif !matched {\n\t\treturn fmt.Errorf(\"must be a valid slug (letters, numbers, and hyphens)\")\n\t}\n\treturn nil\n}\n\ntype UUIDRule struct{}\n\nfunc (r UUIDRule) Validate(value interface{}) error {\n\ts, ok := value.(string)\n\tif !ok {\n\t\treturn fmt.Errorf(\"value must be a string\")\n\t}\n\n\tmatched, _ := regexp.MatchString(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, strings.ToLower(s))\n\tif !matched {\n\t\treturn fmt.Errorf(\"must be a valid UUID\")\n\t}\n\treturn nil\n}\n\ntype MinValueRule struct {\n\tmin int\n}\n\nfunc (r MinValueRule) Validate(value interface{}) error {\n\tnum, ok := value.(int)\n\tif !ok {\n\t\treturn fmt.Errorf(\"value must be an integer\")\n\t}\n\n\tif num < r.min {\n\t\treturn fmt.Errorf(\"value must be at least %d\", r.min)\n\t}\n\treturn nil\n}\n\n// Common rules that can be reused\nvar (\n\tRequired = RequiredRule{}\n\tSlug     = SlugRule{}\n\tUUID     = UUIDRule{}\n\tMinValue = func(min int) MinValueRule {\n\t\treturn MinValueRule{min: min}\n\t}\n)\n"
  },
  {
    "path": "internal/validation/validator_test.go",
    "content": "package validation\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRequiredRule(t *testing.T) {\n\tt.Parallel()\n\n\ttests := map[string]struct {\n\t\tinput   interface{}\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t\"nil value\": {\n\t\t\tinput:   nil,\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"field is required\",\n\t\t},\n\t\t\"empty string\": {\n\t\t\tinput:   \"\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"field is required\",\n\t\t},\n\t\t\"whitespace string\": {\n\t\t\tinput:   \"   \",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"field is required\",\n\t\t},\n\t\t\"valid string\": {\n\t\t\tinput:   \"test\",\n\t\t\twantErr: false,\n\t\t},\n\t\t\"valid number\": {\n\t\t\tinput:   42,\n\t\t\twantErr: false,\n\t\t},\n\t}\n\n\tfor name, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\terr := Required.Validate(tc.input)\n\t\t\tif tc.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t\t} else if !strings.Contains(err.Error(), tc.errMsg) {\n\t\t\t\t\tt.Errorf(\"expected error containing %q, got %q\", tc.errMsg, err.Error())\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"demonstrating multiple errors per field\", func(t *testing.T) {\n\t\tv := New()\n\t\tv.AddRule(\"name\", Required)\n\t\tv.AddRule(\"name\", Slug)\n\n\t\terr := v.Validate(map[string]interface{}{\n\t\t\t\"name\": \"\",\n\t\t})\n\n\t\tif err == nil {\n\t\t\tt.Error(\"expected validation errors but got none\")\n\t\t}\n\n\t\tif validationErrs, ok := err.(ValidationErrors); ok {\n\t\t\tif len(validationErrs) != 2 {\n\t\t\t\tt.Errorf(\"expected 2 validation errors for empty field (Required + Slug), got %d\", len(validationErrs))\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestSlugRule(t *testing.T) {\n\tt.Parallel()\n\n\ttests := map[string]struct {\n\t\tinput   interface{}\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t\"valid slug\": {\n\t\t\tinput:   \"my-slug-123\",\n\t\t\twantErr: false,\n\t\t},\n\t\t\"valid slug with mixed case\": {\n\t\t\tinput:   \"acmE\",\n\t\t\twantErr: false,\n\t\t},\n\t\t\"valid slug with consecutive hyphens\": {\n\t\t\tinput:   \"my--slug\",\n\t\t\twantErr: false,\n\t\t},\n\t\t\"valid slug with a trailing hyphens\": {\n\t\t\tinput:   \"my-slug-\",\n\t\t\twantErr: false,\n\t\t},\n\t\t\"invalid characters\": {\n\t\t\tinput:   \"My Slug!\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"must be a valid slug\",\n\t\t},\n\t\t\"starts with hyphen\": {\n\t\t\tinput:   \"-my-slug\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"must be a valid slug\",\n\t\t},\n\t\t\"non-string input\": {\n\t\t\tinput:   123,\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"value must be a string\",\n\t\t},\n\t}\n\n\tfor name, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\terr := Slug.Validate(tc.input)\n\t\t\tif tc.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t\t} else if !strings.Contains(err.Error(), tc.errMsg) {\n\t\t\t\t\tt.Errorf(\"expected error containing %q, got %q\", tc.errMsg, err.Error())\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMinValueRule(t *testing.T) {\n\tt.Parallel()\n\n\ttests := map[string]struct {\n\t\tinput   interface{}\n\t\tmin     int\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t\"valid value above minimum\": {\n\t\t\tinput:   5,\n\t\t\tmin:     1,\n\t\t\twantErr: false,\n\t\t},\n\t\t\"value equal to minimum\": {\n\t\t\tinput:   1,\n\t\t\tmin:     1,\n\t\t\twantErr: false,\n\t\t},\n\t\t\"value below minimum\": {\n\t\t\tinput:   0,\n\t\t\tmin:     1,\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"value must be at least 1\",\n\t\t},\n\t\t\"non-integer input\": {\n\t\t\tinput:   \"not a number\",\n\t\t\tmin:     1,\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"value must be an integer\",\n\t\t},\n\t}\n\n\tfor name, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\trule := MinValue(tc.min)\n\t\t\terr := rule.Validate(tc.input)\n\t\t\tif tc.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t\t} else if !strings.Contains(err.Error(), tc.errMsg) {\n\t\t\t\t\tt.Errorf(\"expected error containing %q, got %q\", tc.errMsg, err.Error())\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidator(t *testing.T) {\n\tt.Parallel()\n\n\ttests := map[string]struct {\n\t\tfields   map[string]interface{}\n\t\trules    map[string][]Rule\n\t\twantErr  bool\n\t\terrCount int\n\t}{\n\t\t\"single valid field\": {\n\t\t\tfields: map[string]interface{}{\n\t\t\t\t\"name\": \"test-slug\",\n\t\t\t},\n\t\t\trules: map[string][]Rule{\n\t\t\t\t\"name\": {Required, Slug},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t\"multiple valid fields\": {\n\t\t\tfields: map[string]interface{}{\n\t\t\t\t\"name\":    \"test-slug\",\n\t\t\t\t\"count\":   5,\n\t\t\t\t\"enabled\": true,\n\t\t\t},\n\t\t\trules: map[string][]Rule{\n\t\t\t\t\"name\":  {Required, Slug},\n\t\t\t\t\"count\": {MinValue(1)},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t\"single invalid field\": {\n\t\t\tfields: map[string]interface{}{\n\t\t\t\t\"count\": 0,\n\t\t\t},\n\t\t\trules: map[string][]Rule{\n\t\t\t\t\"count\": {MinValue(1)},\n\t\t\t},\n\t\t\twantErr:  true,\n\t\t\terrCount: 1,\n\t\t},\n\t\t\"multiple failures\": {\n\t\t\tfields: map[string]interface{}{\n\t\t\t\t\"name\":  \"\",\n\t\t\t\t\"count\": 0,\n\t\t\t},\n\t\t\trules: map[string][]Rule{\n\t\t\t\t\"name\":  {Required, Slug},\n\t\t\t\t\"count\": {MinValue(1)},\n\t\t\t},\n\t\t\twantErr:  true,\n\t\t\terrCount: 3,\n\t\t},\n\t}\n\n\tfor name, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tv := New()\n\n\t\t\t// Add rules to validator\n\t\t\tfor field, rules := range tc.rules {\n\t\t\t\tfor _, rule := range rules {\n\t\t\t\t\tv.AddRule(field, rule)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr := v.Validate(tc.fields)\n\t\t\tif tc.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t\t}\n\t\t\t\tif validationErrs, ok := err.(ValidationErrors); ok {\n\t\t\t\t\tif len(validationErrs) != tc.errCount {\n\t\t\t\t\t\tt.Errorf(\"expected %d validation errors, got %d\", tc.errCount, len(validationErrs))\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tt.Error(\"expected ValidationErrors type\")\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUUIDRule(t *testing.T) {\n\tt.Parallel()\n\n\ttests := map[string]struct {\n\t\tinput   interface{}\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t\"valid UUID\": {\n\t\t\tinput:   \"123e4567-e89b-12d3-a456-426614174000\",\n\t\t\twantErr: false,\n\t\t},\n\t\t\"invalid format\": {\n\t\t\tinput:   \"not-a-uuid\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"must be a valid UUID\",\n\t\t},\n\t\t\"wrong length\": {\n\t\t\tinput:   \"123e4567-e89b-12d3-a456\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"must be a valid UUID\",\n\t\t},\n\t\t\"non-string input\": {\n\t\t\tinput:   123,\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"value must be a string\",\n\t\t},\n\t}\n\n\tfor name, tc := range tests {\n\t\ttc := tc\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\terr := UUID.Validate(tc.input)\n\t\t\tif tc.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Error(\"expected error but got none\")\n\t\t\t\t} else if !strings.Contains(err.Error(), tc.errMsg) {\n\t\t\t\t\tt.Errorf(\"expected error containing %q, got %q\", tc.errMsg, err.Error())\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "lefthook.yml",
    "content": "pre-commit:\n  parallel: true\n  commands:\n    format:\n      glob: \"*.go\"\n      run: |\n        if command -v gofumpt >/dev/null 2>&1; then\n          gofumpt -w {staged_files}\n        else\n          gofmt -w {staged_files}\n        fi\n      stage_fixed: true\n    lint:\n      glob: \"*.go\"\n      run: golangci-lint run --fix {staged_files}\n      stage_fixed: true\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/kong\"\n\n\t\"github.com/buildkite/cli/v3/cmd/agent\"\n\t\"github.com/buildkite/cli/v3/cmd/api\"\n\t\"github.com/buildkite/cli/v3/cmd/artifacts\"\n\t\"github.com/buildkite/cli/v3/cmd/auth\"\n\t\"github.com/buildkite/cli/v3/cmd/build\"\n\t\"github.com/buildkite/cli/v3/cmd/cluster\"\n\tbkConfig \"github.com/buildkite/cli/v3/cmd/config\"\n\t\"github.com/buildkite/cli/v3/cmd/configure\"\n\tbkInit \"github.com/buildkite/cli/v3/cmd/init\"\n\t\"github.com/buildkite/cli/v3/cmd/job\"\n\t\"github.com/buildkite/cli/v3/cmd/maintainer\"\n\t\"github.com/buildkite/cli/v3/cmd/organization\"\n\t\"github.com/buildkite/cli/v3/cmd/pipeline\"\n\t\"github.com/buildkite/cli/v3/cmd/pkg\"\n\t\"github.com/buildkite/cli/v3/cmd/preflight\"\n\t\"github.com/buildkite/cli/v3/cmd/queue\"\n\t\"github.com/buildkite/cli/v3/cmd/secret\"\n\t\"github.com/buildkite/cli/v3/cmd/skill\"\n\t\"github.com/buildkite/cli/v3/cmd/use\"\n\t\"github.com/buildkite/cli/v3/cmd/user\"\n\tversionPkg \"github.com/buildkite/cli/v3/cmd/version\"\n\t\"github.com/buildkite/cli/v3/cmd/whoami\"\n\t\"github.com/buildkite/cli/v3/internal/cli\"\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\tbkErrors \"github.com/buildkite/cli/v3/internal/errors\"\n\t\"github.com/buildkite/cli/v3/pkg/analytics\"\n)\n\n// Kong CLI structure, with base commands defined as additional commands are defined in their respective files\ntype CLI struct {\n\t// Global flags\n\tYes          bool               `help:\"Skip all confirmation prompts\" short:\"y\"`\n\tNoInput      bool               `help:\"Disable all interactive prompts\" name:\"no-input\"`\n\tQuiet        bool               `help:\"Suppress progress output\" short:\"q\"`\n\tNoPager      bool               `help:\"Disable pager for text output\" name:\"no-pager\"`\n\tDebug        bool               `help:\"Enable debug output for REST API calls\"`\n\tAgent        AgentCmd           `cmd:\"\" help:\"Manage agents\"`\n\tApi          ApiCmd             `cmd:\"\" help:\"Interact with the Buildkite API\"`\n\tArtifacts    ArtifactsCmd       `cmd:\"\" help:\"Manage pipeline build artifacts\"`\n\tAuth         AuthCmd            `cmd:\"\" help:\"Authenticate with Buildkite\"`\n\tBuild        BuildCmd           `cmd:\"\" help:\"Manage pipeline builds\"`\n\tCluster      ClusterCmd         `cmd:\"\" help:\"Manage organization clusters\"`\n\tMaintainer   MaintainerCmd      `cmd:\"\" help:\"Manage cluster maintainers\"`\n\tQueue        QueueCmd           `cmd:\"\" help:\"Manage cluster queues\"`\n\tSecret       SecretCmd          `cmd:\"\" help:\"Manage cluster secrets\"`\n\tSkill        SkillCmd           `cmd:\"\" help:\"Manage Buildkite skills for AI coding agents\"`\n\tConfig       bkConfig.ConfigCmd `cmd:\"\" help:\"Manage CLI configuration\"`\n\tConfigure    ConfigureCmd       `cmd:\"\" help:\"Configure Buildkite API token\" hidden:\"\"`\n\tInit         bkInit.InitCmd     `cmd:\"\" help:\"Initialize a pipeline.yaml file\"`\n\tJob          JobCmd             `cmd:\"\" help:\"Manage jobs within a build\"`\n\tOrganization OrganizationCmd    `cmd:\"\" help:\"Manage organizations\" aliases:\"org\"`\n\tPipeline     PipelineCmd        `cmd:\"\" help:\"Manage pipelines\"`\n\tPackage      PackageCmd         `cmd:\"\" help:\"Manage packages\"`\n\tPreflight    PreflightCmd       `cmd:\"\" help:\"Run a build against a snapshot of the local working tree (experimental)\"`\n\tUse          use.UseCmd         `cmd:\"\" help:\"Select an organization\" hidden:\"\"`\n\tUser         UserCmd            `cmd:\"\" help:\"Invite users to the organization\"`\n\tVersion      VersionCmd         `cmd:\"\" help:\"Print the version of the CLI being used\"`\n\tWhoami       whoami.WhoAmICmd   `cmd:\"\" help:\"Print the current user and organization\" hidden:\"\"`\n}\n\ntype (\n\tVersionCmd struct {\n\t\tversionPkg.VersionCmd `cmd:\"\" help:\"Print the version of the CLI being used\"`\n\t}\n\tAuthCmd struct {\n\t\tLogin  auth.LoginCmd  `cmd:\"\" help:\"Login to Buildkite using OAuth or an API token\"`\n\t\tLogout auth.LogoutCmd `cmd:\"\" help:\"Logout and remove stored credentials\"`\n\t\tStatus auth.StatusCmd `cmd:\"\" help:\"Print the current user auth status\"`\n\t\tSwitch auth.SwitchCmd `cmd:\"\" help:\"Switch to a different organization\" aliases:\"use\"`\n\t\tToken  auth.TokenCmd  `cmd:\"\" help:\"Print the stored API token for the current organization\"`\n\t}\n\tAgentCmd struct {\n\t\tInstall agent.InstallCmd `cmd:\"\" help:\"Install the buildkite-agent binary locally.\"`\n\t\tRun     agent.RunCmd     `cmd:\"\" help:\"Run an ephemeral buildkite-agent locally.\"`\n\t\tPause   agent.PauseCmd   `cmd:\"\" help:\"Pause a Buildkite agent.\"`\n\t\tList    agent.ListCmd    `cmd:\"\" help:\"List agents.\" alias:\"ls\"`\n\t\tResume  agent.ResumeCmd  `cmd:\"\" help:\"Resume a Buildkite agent.\"`\n\t\tStop    agent.StopCmd    `cmd:\"\" help:\"Stop Buildkite agents.\"`\n\t\tView    agent.ViewCmd    `cmd:\"\" help:\"View details of an agent.\"`\n\t}\n\tApiCmd struct {\n\t\tapi.ApiCmd `cmd:\"\" help:\"Interact with the Buildkite API\"`\n\t}\n\tArtifactsCmd struct {\n\t\tDownload artifacts.DownloadCmd `cmd:\"\" help:\"Download artifacts from a build.\"`\n\t\tList     artifacts.ListCmd     `cmd:\"\" help:\"List artifacts for a build or a job in a build.\" aliases:\"ls\"`\n\t}\n\tBuildCmd struct {\n\t\tCreate   build.CreateCmd   `cmd:\"\" aliases:\"new\" help:\"Create a new build.\"` // Aliasing \"new\" because we've renamed this to \"create\", but we need to support backwards compatibility\n\t\tCancel   build.CancelCmd   `cmd:\"\" help:\"Cancel a build.\"`\n\t\tView     build.ViewCmd     `cmd:\"\" help:\"View build information.\"`\n\t\tList     build.ListCmd     `cmd:\"\" help:\"List builds.\" aliases:\"ls\"`\n\t\tDownload build.DownloadCmd `cmd:\"\" help:\"Download resources for a build.\"`\n\t\tRebuild  build.RebuildCmd  `cmd:\"\" help:\"Rebuild a build.\"`\n\t\tWatch    build.WatchCmd    `cmd:\"\" help:\"Watch a build's progress in real-time.\"`\n\t}\n\tClusterCmd struct {\n\t\tList   cluster.ListCmd   `cmd:\"\" help:\"List clusters.\" aliases:\"ls\"`\n\t\tView   cluster.ViewCmd   `cmd:\"\" help:\"View cluster information.\"`\n\t\tCreate cluster.CreateCmd `cmd:\"\" help:\"Create a new cluster.\"`\n\t\tUpdate cluster.UpdateCmd `cmd:\"\" help:\"Update a cluster.\"`\n\t\tDelete cluster.DeleteCmd `cmd:\"\" help:\"Delete a cluster.\" aliases:\"rm\"`\n\t}\n\tConfigureCmd struct {\n\t\tconfigure.ConfigureCmd `cmd:\"\" help:\"Configure Buildkite API token\"`\n\t}\n\tJobCmd struct {\n\t\tCancel       job.CancelCmd       `cmd:\"\" help:\"Cancel a job.\"`\n\t\tList         job.ListCmd         `cmd:\"\" help:\"List jobs.\" aliases:\"ls\"`\n\t\tLog          job.LogCmd          `cmd:\"\" help:\"Get logs for a job.\"`\n\t\tReprioritize job.ReprioritizeCmd `cmd:\"\" help:\"Reprioritize a job.\" aliases:\"priority\"`\n\t\tRetry        job.RetryCmd        `cmd:\"\" help:\"Retry a job.\"`\n\t\tUnblock      job.UnblockCmd      `cmd:\"\" help:\"Unblock a job.\"`\n\t}\n\tMaintainerCmd struct {\n\t\tList   maintainer.ListCmd   `cmd:\"\" help:\"List cluster maintainers.\" aliases:\"ls\"`\n\t\tCreate maintainer.CreateCmd `cmd:\"\" help:\"Create a cluster maintainer.\"`\n\t\tDelete maintainer.DeleteCmd `cmd:\"\" help:\"Delete a cluster maintainer.\" aliases:\"rm\"`\n\t}\n\tOrganizationCmd struct {\n\t\tList organization.ListCmd `cmd:\"\" help:\"List configured organizations.\" aliases:\"ls\"`\n\t}\n\tPackageCmd struct {\n\t\tPush pkg.PushCmd `cmd:\"\" help:\"Push a new package to a Buildkite registry\"`\n\t}\n\tPipelineCmd struct {\n\t\tCopy     pipeline.CopyCmd     `cmd:\"\" help:\"Copy an existing pipeline.\" aliases:\"cp\"`\n\t\tCreate   pipeline.CreateCmd   `cmd:\"\" help:\"Create a new pipeline.\"`\n\t\tList     pipeline.ListCmd     `cmd:\"\" help:\"List pipelines.\" aliases:\"ls\"`\n\t\tConvert  pipeline.ConvertCmd  `cmd:\"\" help:\"Convert a CI/CD pipeline configuration to Buildkite format.\" aliases:\"migrate\"`\n\t\tValidate pipeline.ValidateCmd `cmd:\"\" help:\"Validate a pipeline YAML file.\"`\n\t\tView     pipeline.ViewCmd     `cmd:\"\" help:\"View a pipeline.\"`\n\t}\n\tPreflightCmd struct {\n\t\tRun     preflight.RunCmd     `cmd:\"\" default:\"withargs\" help:\"Run a build against a snapshot of the local working tree (experimental)\"`\n\t\tCleanup preflight.CleanupCmd `cmd:\"\" help:\"Clean up completed preflight branches (experimental)\"`\n\t}\n\tQueueCmd struct {\n\t\tList   queue.ListCmd   `cmd:\"\" help:\"List cluster queues.\" aliases:\"ls\"`\n\t\tView   queue.ViewCmd   `cmd:\"\" help:\"View a cluster queue.\"`\n\t\tCreate queue.CreateCmd `cmd:\"\" help:\"Create a new cluster queue.\"`\n\t\tUpdate queue.UpdateCmd `cmd:\"\" help:\"Update a cluster queue.\"`\n\t\tDelete queue.DeleteCmd `cmd:\"\" help:\"Delete a cluster queue.\" aliases:\"rm\"`\n\t\tPause  queue.PauseCmd  `cmd:\"\" help:\"Pause dispatch for a cluster queue.\"`\n\t\tResume queue.ResumeCmd `cmd:\"\" help:\"Resume dispatch for a cluster queue.\"`\n\t}\n\tSecretCmd struct {\n\t\tList   secret.ListCmd   `cmd:\"\" help:\"List secrets for a cluster.\" aliases:\"ls\"`\n\t\tGet    secret.GetCmd    `cmd:\"\" help:\"View a cluster secret.\"`\n\t\tCreate secret.CreateCmd `cmd:\"\" help:\"Create a new cluster secret.\"`\n\t\tUpdate secret.UpdateCmd `cmd:\"\" help:\"Update a cluster secret.\"`\n\t\tDelete secret.DeleteCmd `cmd:\"\" help:\"Delete a cluster secret.\" aliases:\"rm\"`\n\t}\n\tSkillCmd struct {\n\t\tAdd    skill.AddCmd    `cmd:\"\" help:\"Install a Buildkite skill.\"`\n\t\tUpdate skill.UpdateCmd `cmd:\"\" help:\"Update an installed Buildkite skill.\"`\n\t\tDelete skill.DeleteCmd `cmd:\"\" help:\"Delete an installed Buildkite skill.\" aliases:\"rm\"`\n\t}\n\tUserCmd struct {\n\t\tInvite user.InviteCmd `cmd:\"\" help:\"Invite users to your organization.\"`\n\t}\n)\n\nfunc (c PreflightCmd) Help() string {\n\treturn preflight.HelpText()\n}\n\nfunc handleError(err error) {\n\tbkErrors.NewHandler().Handle(err)\n}\n\nfunc newKongParser(cli *CLI, options ...kong.Option) (*kong.Kong, error) {\n\tbaseOptions := []kong.Option{\n\t\tkong.Name(\"bk\"),\n\t\tkong.Description(\"Work with Buildkite from the command line.\"),\n\t\tkong.Vars{\n\t\t\t// Empty default allows commands to fall back to config value\n\t\t\t\"output_default_format\": \"\",\n\t\t\t\"skill_repo\":            \"buildkite/skills\",\n\t\t\t\"skill_branch\":          \"main\",\n\t\t},\n\t}\n\tbaseOptions = append(baseOptions, options...)\n\n\treturn kong.New(cli, baseOptions...)\n}\n\nfunc renderHelp(args []string) (string, error) {\n\tcli := &CLI{}\n\tvar stdout, stderr bytes.Buffer\n\tparser, err := newKongParser(\n\t\tcli,\n\t\tkong.Writers(&stdout, &stderr),\n\t\tkong.Exit(func(int) {}),\n\t)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tapplyExperiments(parser, config.New(nil, nil))\n\tif _, err := parser.Parse(args); err != nil {\n\t\tif stdout.Len() > 0 {\n\t\t\treturn stdout.String(), nil\n\t\t}\n\t\treturn \"\", err\n\t}\n\treturn stdout.String(), nil\n}\n\nfunc renderPreflightHelp() (string, error) {\n\tparentHelp, err := renderHelp([]string{\"preflight\", \"--help\"})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\trunHelp, err := renderHelp([]string{\"preflight\", \"run\", \"--help\"})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tparentFlagsStart := strings.Index(parentHelp, \"\\nFlags:\\n\")\n\tparentCommandsStart := strings.Index(parentHelp, \"\\nCommands:\\n\")\n\trunFlagsStart := strings.Index(runHelp, \"\\nFlags:\\n\")\n\tif parentFlagsStart == -1 || parentCommandsStart == -1 || runFlagsStart == -1 {\n\t\treturn parentHelp, nil\n\t}\n\n\treturn parentHelp[:parentFlagsStart] + runHelp[runFlagsStart:] + parentHelp[parentCommandsStart:], nil\n}\n\nfunc isPreflightHelpRequest(args []string) bool {\n\tswitch {\n\tcase len(args) == 2 && args[0] == \"preflight\" && (args[1] == \"--help\" || args[1] == \"-h\"):\n\t\treturn true\n\tcase len(args) == 2 && args[0] == \"help\" && args[1] == \"preflight\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// applyExperiments toggles visibility of experimental commands based on config.\nfunc applyExperiments(parser *kong.Kong, conf *config.Config) {\n\tfor _, node := range parser.Model.Children {\n\t\tswitch node.Name {\n\t\tcase config.ExperimentPreflight:\n\t\t\tnode.Hidden = !conf.HasExperiment(config.ExperimentPreflight)\n\t\t}\n\t}\n}\n\nfunc main() {\n\tos.Exit(run())\n}\n\nfunc run() int {\n\t// Handle no-args and \"help\" cases by showing help instead of error\n\t// This addresses the Kong limitation described in https://github.com/alecthomas/kong/issues/33\n\tif len(os.Args) <= 1 || (len(os.Args) == 2 && os.Args[1] == \"help\") {\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Error: %v\\n\", err)\n\t\t\treturn 1\n\t\t}\n\t\tapplyExperiments(parser, config.New(nil, nil))\n\t\t_, _ = parser.Parse([]string{\"--help\"})\n\t\treturn 0\n\t}\n\n\t// Handle --version and -V flags at the top level\n\tif len(os.Args) == 2 && (os.Args[1] == \"--version\" || os.Args[1] == \"-V\") {\n\t\tfmt.Print(versionPkg.Format(versionPkg.Version))\n\t\treturn 0\n\t}\n\n\targs := os.Args[1:]\n\n\tif isPreflightHelpRequest(args) {\n\t\thelp, err := renderPreflightHelp()\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, \"Error: %v\\n\", err)\n\t\t\treturn 1\n\t\t}\n\t\tfmt.Print(help)\n\t\treturn 0\n\t}\n\n\tcliInstance := &CLI{}\n\n\tconf := config.New(nil, nil)\n\n\ttracker := analytics.Init(\"dev\", conf.TelemetryEnabled())\n\tdefer tracker.Close()\n\ttracker.SetOrg(conf.OrganizationSlug())\n\n\tparser, err := newKongParser(cliInstance)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Error: %v\\n\", err)\n\t\treturn 1\n\t}\n\tapplyExperiments(parser, conf)\n\tctx, err := parser.Parse(args)\n\tif err != nil {\n\t\ttracker.TrackCommand(\"unknown command\", args, nil)\n\n\t\tvar parseErr *kong.ParseError\n\t\tif errors.As(err, &parseErr) && !strings.Contains(err.Error(), \"did you mean\") {\n\t\t\t_ = parseErr.Context.PrintUsage(false)\n\t\t\tfmt.Fprintln(os.Stderr)\n\t\t}\n\n\t\tfmt.Fprintf(os.Stderr, \"Error: %v\\n\", err)\n\t\treturn 1\n\t}\n\n\ttracker.TrackCommand(analytics.ParseSubcommand(ctx.Command()), args, nil)\n\n\tglobals := cli.Globals{\n\t\tYes:     cliInstance.Yes,\n\t\tNoInput: cliInstance.NoInput,\n\t\tQuiet:   cliInstance.Quiet,\n\t\tNoPager: cliInstance.NoPager,\n\t\tDebug:   cliInstance.Debug,\n\t}\n\n\tctx.BindTo(cli.GlobalFlags(globals), (*cli.GlobalFlags)(nil))\n\n\tif err := ctx.Run(cliInstance); err != nil {\n\t\thandleError(err)\n\t\treturn 1\n\t}\n\treturn 0\n}\n"
  },
  {
    "path": "main_test.go",
    "content": "package main\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\t\"github.com/spf13/afero\"\n)\n\nfunc unsetEnv(t *testing.T, key string) {\n\tt.Helper()\n\toriginal, had := os.LookupEnv(key)\n\tif had {\n\t\tif err := os.Unsetenv(key); err != nil {\n\t\t\tt.Fatalf(\"failed to unset env %s: %v\", key, err)\n\t\t}\n\t}\n\tt.Cleanup(func() {\n\t\tvar err error\n\t\tif had {\n\t\t\terr = os.Setenv(key, original)\n\t\t} else {\n\t\t\terr = os.Unsetenv(key)\n\t\t}\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to restore env %s: %v\", key, err)\n\t\t}\n\t})\n}\n\nfunc TestApplyExperiments(t *testing.T) {\n\tt.Run(\"preflight visible by default\", func(t *testing.T) {\n\t\tunsetEnv(t, \"BUILDKITE_EXPERIMENTS\")\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create parser: %v\", err)\n\t\t}\n\n\t\tapplyExperiments(parser, conf)\n\n\t\tfor _, node := range parser.Model.Children {\n\t\t\tif node.Name == \"preflight\" {\n\t\t\t\tif node.Hidden {\n\t\t\t\t\tt.Error(\"preflight should be visible by default\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tt.Fatal(\"preflight command not found in parser\")\n\t})\n\n\tt.Run(\"preflight hidden when experiment disabled\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"alpha\")\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create parser: %v\", err)\n\t\t}\n\n\t\tapplyExperiments(parser, conf)\n\n\t\tfor _, node := range parser.Model.Children {\n\t\t\tif node.Name == \"preflight\" {\n\t\t\t\tif !node.Hidden {\n\t\t\t\t\tt.Error(\"preflight should be hidden when experiment is disabled\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tt.Fatal(\"preflight command not found in parser\")\n\t})\n\n\tt.Run(\"preflight hidden when experiments override is empty\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"\")\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create parser: %v\", err)\n\t\t}\n\n\t\tapplyExperiments(parser, conf)\n\n\t\tfor _, node := range parser.Model.Children {\n\t\t\tif node.Name == \"preflight\" {\n\t\t\t\tif !node.Hidden {\n\t\t\t\t\tt.Error(\"preflight should be hidden when experiments override is empty\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tt.Fatal(\"preflight command not found in parser\")\n\t})\n\n\tt.Run(\"preflight visible when experiment enabled explicitly\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_EXPERIMENTS\", \"preflight\")\n\t\tfs := afero.NewMemMapFs()\n\t\tconf := config.New(fs, nil)\n\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create parser: %v\", err)\n\t\t}\n\n\t\tapplyExperiments(parser, conf)\n\n\t\tfor _, node := range parser.Model.Children {\n\t\t\tif node.Name == \"preflight\" {\n\t\t\t\tif node.Hidden {\n\t\t\t\t\tt.Error(\"preflight should be visible when experiment is enabled\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tt.Fatal(\"preflight command not found in parser\")\n\t})\n\n\tt.Run(\"preflight root still parses with default subcommand\", func(t *testing.T) {\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create parser: %v\", err)\n\t\t}\n\n\t\tif _, err := parser.Parse([]string{\"preflight\"}); err != nil {\n\t\t\tt.Fatalf(\"failed to parse preflight root command: %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"preflight await-test-results parses without a value\", func(t *testing.T) {\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create parser: %v\", err)\n\t\t}\n\n\t\tif _, err := parser.Parse([]string{\"preflight\", \"--await-test-results\"}); err != nil {\n\t\t\tt.Fatalf(\"failed to parse preflight await-test-results flag: %v\", err)\n\t\t}\n\t\tif !cli.Preflight.Run.AwaitTestResults.Enabled {\n\t\t\tt.Fatal(\"expected await-test-results to be enabled\")\n\t\t}\n\t\tif cli.Preflight.Run.AwaitTestResults.Duration != 30*time.Second {\n\t\t\tt.Fatalf(\"expected default await-test-results duration, got %s\", cli.Preflight.Run.AwaitTestResults.Duration)\n\t\t}\n\t})\n\n\tt.Run(\"preflight await-test-results parses with an explicit duration\", func(t *testing.T) {\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create parser: %v\", err)\n\t\t}\n\n\t\tif _, err := parser.Parse([]string{\"preflight\", \"--await-test-results=45s\"}); err != nil {\n\t\t\tt.Fatalf(\"failed to parse preflight await-test-results duration: %v\", err)\n\t\t}\n\t\tif !cli.Preflight.Run.AwaitTestResults.Enabled {\n\t\t\tt.Fatal(\"expected await-test-results to be enabled\")\n\t\t}\n\t\tif cli.Preflight.Run.AwaitTestResults.Duration != 45*time.Second {\n\t\t\tt.Fatalf(\"expected explicit await-test-results duration, got %s\", cli.Preflight.Run.AwaitTestResults.Duration)\n\t\t}\n\t})\n\n\tt.Run(\"preflight exit-on parses repeated flags\", func(t *testing.T) {\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create parser: %v\", err)\n\t\t}\n\n\t\tif _, err := parser.Parse([]string{\"preflight\", \"--exit-on=build-failing\", \"--exit-on=build-failing\"}); err != nil {\n\t\t\tt.Fatalf(\"failed to parse repeated preflight exit-on flags: %v\", err)\n\t\t}\n\t\tif len(cli.Preflight.Run.ExitOn) != 2 {\n\t\t\tt.Fatalf(\"expected 2 exit-on values, got %d\", len(cli.Preflight.Run.ExitOn))\n\t\t}\n\t})\n\n\tt.Run(\"preflight exit-on rejects unknown values\", func(t *testing.T) {\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create parser: %v\", err)\n\t\t}\n\n\t\tif _, err := parser.Parse([]string{\"preflight\", \"--exit-on=test-failed:3\"}); err == nil {\n\t\t\tt.Fatal(\"expected parse error for invalid exit-on value\")\n\t\t}\n\t})\n\n\tt.Run(\"preflight exit-on rejects incompatible combinations\", func(t *testing.T) {\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create parser: %v\", err)\n\t\t}\n\n\t\tif _, err := parser.Parse([]string{\"preflight\", \"--exit-on=build-failing\", \"--exit-on=build-terminal\"}); err == nil {\n\t\t\tt.Fatal(\"expected parse error for incompatible exit-on values\")\n\t\t}\n\t})\n\n\tt.Run(\"preflight run subcommand still parses\", func(t *testing.T) {\n\t\tcli := &CLI{}\n\t\tparser, err := newKongParser(cli)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create parser: %v\", err)\n\t\t}\n\n\t\tif _, err := parser.Parse([]string{\"preflight\", \"run\", \"--await-test-results=45s\"}); err != nil {\n\t\t\tt.Fatalf(\"failed to parse preflight run subcommand: %v\", err)\n\t\t}\n\t\tif !cli.Preflight.Run.AwaitTestResults.Enabled {\n\t\t\tt.Fatal(\"expected run subcommand await-test-results to be enabled\")\n\t\t}\n\t\tif cli.Preflight.Run.AwaitTestResults.Duration != 45*time.Second {\n\t\t\tt.Fatalf(\"expected explicit run subcommand await-test-results duration, got %s\", cli.Preflight.Run.AwaitTestResults.Duration)\n\t\t}\n\t})\n\n\tt.Run(\"preflight help includes mirrored run flags\", func(t *testing.T) {\n\t\thelp, err := renderPreflightHelp()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to render preflight help: %v\", err)\n\t\t}\n\t\tfor _, want := range []string{\n\t\t\t\"--[no-]watch\",\n\t\t\t\"--exit-on=EXIT-ON,...\",\n\t\t\t\"--await-test-results\",\n\t\t\t\"--no-cleanup\",\n\t\t\t\"preflight cleanup [flags]\",\n\t\t} {\n\t\t\tif !strings.Contains(help, want) {\n\t\t\t\tt.Fatalf(\"expected preflight help to contain %q, got:\\n%s\", want, help)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"preflight help requests are detected\", func(t *testing.T) {\n\t\ttests := []struct {\n\t\t\targs []string\n\t\t\twant bool\n\t\t}{\n\t\t\t{args: []string{\"preflight\", \"--help\"}, want: true},\n\t\t\t{args: []string{\"preflight\", \"-h\"}, want: true},\n\t\t\t{args: []string{\"help\", \"preflight\"}, want: true},\n\t\t\t{args: []string{\"preflight\", \"run\", \"--help\"}, want: false},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tif got := isPreflightHelpRequest(tt.args); got != tt.want {\n\t\t\t\tt.Fatalf(\"isPreflightHelpRequest(%q) = %v, want %v\", tt.args, got, tt.want)\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "mise.toml",
    "content": "# Pinned local toolchain for contributors. The Go version matches the\n# CI and release toolchain; the module minimum stays in go.mod.\n[settings]\nexperimental = true\nlockfile = true\n\n[tools]\ngo = \"1.26.3\"\ngolangci-lint = \"2.12.2\"\nlefthook = \"2.1.6\"\n\"aqua:mvdan/gofumpt\" = \"0.10.0\"\n\"go:github.com/nikolaydubina/go-cover-treemap\" = \"1.5.1\"\n\"github:goreleaser/goreleaser-pro\" = \"2.15.4\"\nko = \"0.18.1\"\n\n[tasks.format]\ndescription = \"Format Go files with gofumpt\"\nrun = \"gofumpt -w .\"\n\n[tasks.build]\ndescription = \"Build a local bk binary into dist/\"\nrun = \"VERSION=\\\"$(git describe --tags --dirty --always 2>/dev/null || echo DEV)\\\"; mkdir -p dist && go build -ldflags \\\"-X github.com/buildkite/cli/v3/cmd/version.Version=$VERSION\\\" -o dist/bk .\"\n\n[tasks.install]\ndescription = \"Install the local bk binary into the active Go bin directory\"\ndepends = [\"build\"]\nrun = \"BIN_DIR=\\\"$(go env GOBIN)\\\"; if [ -z \\\"$BIN_DIR\\\" ]; then BIN_DIR=\\\"$(go env GOPATH)/bin\\\"; fi; mkdir -p \\\"$BIN_DIR\\\"; install -m 0755 dist/bk \\\"$BIN_DIR/bk\\\"\"\n\n[tasks.\"install:global\"]\ndescription = \"Install the local bk binary into ~/bin\"\ndepends = [\"build\"]\nrun = \"mkdir -p \\\"$HOME/bin\\\"; install -m 0755 dist/bk \\\"$HOME/bin/bk\\\"\"\n\n[tasks.lint]\ndescription = \"Run golangci-lint with the repository configuration\"\nrun = \"golangci-lint run --verbose --timeout 3m\"\n\n[tasks.test]\ndescription = \"Run the Go test suite\"\nrun = \"go test ./...\"\n\n[tasks.generate]\ndescription = \"Regenerate the GraphQL client code\"\nrun = \"go generate ./cmd/generate\"\n\n[tasks.hooks]\ndescription = \"Install the repository git hooks\"\nrun = \"lefthook install\"\n\n[tasks.ci]\ndescription = \"Run the main local CI checks\"\ndepends = [\"lint\", \"test\"]\n"
  },
  {
    "path": "pkg/analytics/analytics.go",
    "content": "package analytics\n\nimport (\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/posthog/posthog-go\"\n)\n\nvar (\n\t// Set via -ldflags at build time: -X github.com/buildkite/cli/v3/pkg/analytics.apiKey=...\n\tapiKey  = \"\"\n\tapiHost = \"https://us.i.posthog.com\"\n)\n\nvar (\n\tclient posthog.Client\n\tonce   sync.Once\n)\n\ntype Client struct {\n\tposthog  posthog.Client\n\tdisabled bool\n\tuserID   string\n\torg      string\n}\n\nfunc Init(version string, enabled bool) *Client {\n\tif !enabled {\n\t\treturn &Client{disabled: true}\n\t}\n\n\tkey := apiKey\n\tif envKey := os.Getenv(\"BK_ANALYTICS_KEY\"); envKey != \"\" {\n\t\tkey = envKey\n\t}\n\n\tif key == \"\" || os.Getenv(\"CI\") != \"\" {\n\t\treturn &Client{disabled: true}\n\t}\n\n\tonce.Do(func() {\n\t\tvar err error\n\t\tclient, err = posthog.NewWithConfig(key, posthog.Config{\n\t\t\tEndpoint: apiHost,\n\t\t\tLogger:   noopLogger{},\n\t\t})\n\t\tif err != nil {\n\t\t\tclient = nil\n\t\t}\n\t})\n\n\tif client == nil {\n\t\treturn &Client{disabled: true}\n\t}\n\n\treturn &Client{\n\t\tposthog: client,\n\t\tuserID:  getUserID(),\n\t}\n}\n\nfunc (c *Client) SetOrg(org string) {\n\tif c.disabled {\n\t\treturn\n\t}\n\tc.org = org\n}\n\nfunc (c *Client) TrackCommand(subcommand string, fullArgs []string, properties map[string]interface{}) {\n\tif c.disabled || c.posthog == nil {\n\t\treturn\n\t}\n\n\tprops := posthog.NewProperties()\n\tprops.Set(\"command\", strings.Join(fullArgs, \" \"))\n\tprops.Set(\"channel\", \"cli\")\n\tprops.Set(\"os\", runtime.GOOS)\n\tprops.Set(\"arch\", runtime.GOARCH)\n\tif c.org != \"\" {\n\t\tprops.Set(\"organization\", c.org)\n\t}\n\n\tfor k, v := range properties {\n\t\tprops.Set(k, v)\n\t}\n\n\t_ = c.posthog.Enqueue(posthog.Capture{\n\t\tDistinctId: c.userID,\n\t\tEvent:      subcommand,\n\t\tProperties: props,\n\t})\n}\n\nfunc (c *Client) Close() {\n\tif c.disabled || c.posthog == nil {\n\t\treturn\n\t}\n\t_ = c.posthog.Close()\n}\n\nfunc getUserID() string {\n\tif id := os.Getenv(\"BUILDKITE_BUILD_ID\"); id != \"\" {\n\t\treturn \"build:\" + id\n\t}\n\n\thostname, err := os.Hostname()\n\tif err != nil {\n\t\treturn \"anonymous\"\n\t}\n\treturn \"host:\" + hostname\n}\n\n// ParseSubcommand extracts the subcommand path from Kong's command string,\n// removing angle-bracket arguments like \"<pipeline>\".\nfunc ParseSubcommand(kongCommand string) string {\n\tparts := strings.Fields(kongCommand)\n\tvar cmdParts []string\n\tfor _, p := range parts {\n\t\tif !strings.HasPrefix(p, \"<\") {\n\t\t\tcmdParts = append(cmdParts, p)\n\t\t}\n\t}\n\treturn strings.Join(cmdParts, \" \")\n}\n"
  },
  {
    "path": "pkg/analytics/logger.go",
    "content": "package analytics\n\n// noopLogger implements the posthog.Logger interface\n// It suppresses all PostHog SDK logs\ntype noopLogger struct{}\n\nfunc (noopLogger) Debugf(format string, args ...interface{}) {}\n\nfunc (noopLogger) Logf(format string, args ...interface{}) {}\n\nfunc (noopLogger) Warnf(format string, args ...interface{}) {}\n\nfunc (noopLogger) Errorf(format string, args ...interface{}) {}\n"
  },
  {
    "path": "pkg/cmd/factory/factory.go",
    "content": "package factory\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/Khan/genqlient/graphql\"\n\t\"github.com/buildkite/cli/v3/cmd/version\"\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\tbkhttp \"github.com/buildkite/cli/v3/internal/http\"\n\t\"github.com/buildkite/cli/v3/pkg/keyring\"\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n\tgit \"github.com/go-git/go-git/v5\"\n)\n\nvar baseUserAgent string\n\ntype Factory struct {\n\tConfig        *config.Config\n\tGitRepository *git.Repository\n\tGraphQLClient graphql.Client\n\tRestAPIClient *buildkite.Client\n\tVersion       string\n\tSkipConfirm   bool\n\tNoInput       bool\n\tQuiet         bool\n\tNoPager       bool\n\tDebug         bool\n}\n\n// FactoryOpt is a functional option for configuring the Factory\ntype FactoryOpt func(*factoryConfig)\n\ntype factoryConfig struct {\n\tdebug           bool\n\torgOverride     string\n\ttransport       http.RoundTripper\n\tuserAgentSuffix string\n}\n\n// WithDebug enables debug output for REST API calls\nfunc WithDebug(debug bool) FactoryOpt {\n\treturn func(c *factoryConfig) {\n\t\tc.debug = debug\n\t}\n}\n\n// WithOrgOverride overrides the configured organization slug for API token\n// resolution. When set, the factory will use the token for this org instead\n// of the currently selected org.\nfunc WithOrgOverride(org string) FactoryOpt {\n\treturn func(c *factoryConfig) {\n\t\tc.orgOverride = org\n\t}\n}\n\n// WithTransport sets a custom http.RoundTripper for the REST API client.\n// It is composed with the debug transport when debug mode is enabled.\nfunc WithTransport(t http.RoundTripper) FactoryOpt {\n\treturn func(c *factoryConfig) {\n\t\tc.transport = t\n\t}\n}\n\n// WithUserAgentSuffix appends an extra product token to the default user agent.\nfunc WithUserAgentSuffix(suffix string) FactoryOpt {\n\treturn func(c *factoryConfig) {\n\t\tc.userAgentSuffix = suffix\n\t}\n}\n\n// debugTransport wraps an http.RoundTripper and logs requests/responses with sensitive headers redacted\ntype debugTransport struct {\n\ttransport http.RoundTripper\n}\n\n// sensitiveHeaders contains headers that should be redacted in debug output\nvar sensitiveHeaders = []string{\"Authorization\"}\n\nfunc (d *debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\t// Save and restore the request body so that dumping it does not consume\n\t// the body before the real transport sends it. req.Clone() shares the\n\t// underlying Body reader, so DumpRequestOut on a clone drains the\n\t// original — leading to an empty/malformed request reaching the server.\n\tvar bodyBytes []byte\n\tif req.Body != nil {\n\t\tvar err error\n\t\tbodyBytes, err = io.ReadAll(req.Body)\n\t\treq.Body.Close()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"debug transport: reading request body: %w\", err)\n\t\t}\n\t\t// Restore the body for the actual request\n\t\treq.Body = io.NopCloser(bytes.NewReader(bodyBytes))\n\t}\n\n\t// Build a clone with its own copy of the body for dumping\n\treqCopy := req.Clone(req.Context())\n\tredactHeaders(reqCopy.Header)\n\tif bodyBytes != nil {\n\t\treqCopy.Body = io.NopCloser(bytes.NewReader(bodyBytes))\n\t}\n\n\tif dump, err := httputil.DumpRequestOut(reqCopy, true); err == nil {\n\t\tfmt.Fprintf(os.Stderr, \"DEBUG request uri=%s\\n%s\\n\", req.URL, redactBody(string(dump)))\n\t}\n\n\tresp, err := d.transport.RoundTrip(req)\n\tif err != nil {\n\t\treturn resp, err\n\t}\n\n\tif dump, err := httputil.DumpResponse(resp, true); err == nil {\n\t\tfmt.Fprintf(os.Stderr, \"DEBUG response uri=%s\\n%s\\n\", req.URL, redactBody(string(dump)))\n\t}\n\n\treturn resp, nil\n}\n\n// sensitiveBodyPatterns matches token values in form-encoded request bodies\n// and JSON response bodies that should be redacted in debug output.\nvar sensitiveBodyPatterns = regexp.MustCompile(\n\t`((?:refresh_token|access_token|code|code_verifier)=)[^&\\s]+` +\n\t\t`|(\"(?:access_token|refresh_token|code)\":\\s*\")[^\"]+(\"?)`,\n)\n\n// redactBody replaces sensitive token values in HTTP dumps.\nfunc redactBody(dump string) string {\n\treturn sensitiveBodyPatterns.ReplaceAllStringFunc(dump, func(match string) string {\n\t\t// Form-encoded: key=value\n\t\tif idx := strings.IndexByte(match, '='); idx > 0 && !strings.HasPrefix(match, `\"`) {\n\t\t\treturn match[:idx+1] + \"[REDACTED]\"\n\t\t}\n\t\t// JSON: \"key\": \"value\"\n\t\treturn sensitiveBodyPatterns.ReplaceAllString(match, `${1}[REDACTED]${2}`)\n\t})\n}\n\n// redactHeaders replaces sensitive header values with [REDACTED]\nfunc redactHeaders(headers http.Header) {\n\tfor _, header := range sensitiveHeaders {\n\t\tif values := headers.Values(header); len(values) > 0 {\n\t\t\tfor i, v := range values {\n\t\t\t\t// Keep the auth type (Bearer, Basic, etc.) but redact the token\n\t\t\t\tif parts := strings.SplitN(v, \" \", 2); len(parts) == 2 {\n\t\t\t\t\theaders[header][i] = parts[0] + \" [REDACTED]\"\n\t\t\t\t} else {\n\t\t\t\t\theaders[header][i] = \"[REDACTED]\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype gqlHTTPClient struct {\n\tclient *http.Client\n}\n\nfunc init() {\n\tbaseUserAgent = fmt.Sprintf(\"%s buildkite-cli/%s\", buildkite.DefaultUserAgent, version.Version)\n}\n\nfunc buildUserAgent(suffix string) string {\n\tif suffix == \"\" {\n\t\treturn baseUserAgent\n\t}\n\treturn fmt.Sprintf(\"%s %s\", baseUserAgent, suffix)\n}\n\nfunc (a *gqlHTTPClient) Do(req *http.Request) (*http.Response, error) {\n\t// Auth and User-Agent are injected by AuthTransport in the\n\t// shared HTTP transport chain, so we don't set them here.\n\treturn a.client.Do(req)\n}\n\nfunc New(opts ...FactoryOpt) (*Factory, error) {\n\tcfg := &factoryConfig{}\n\tfor _, opt := range opts {\n\t\topt(cfg)\n\t}\n\n\trepo, err := git.PlainOpenWithOptions(\".\", &git.PlainOpenOptions{DetectDotGit: true, EnableDotGitCommonDir: true})\n\tif err != nil {\n\t\tif err == git.ErrRepositoryNotExists {\n\t\t\trepo = nil\n\t\t}\n\t}\n\n\tconf := config.New(nil, repo)\n\n\ttoken := conf.APIToken()\n\tif cfg.orgOverride != \"\" {\n\t\tif t := conf.APITokenForOrg(cfg.orgOverride); t != \"\" {\n\t\t\ttoken = t\n\t\t}\n\t}\n\n\tuserAgent := buildUserAgent(cfg.userAgentSuffix)\n\n\t// Build the HTTP transport chain.\n\t//\n\t// The chain is (outermost first):\n\t//   RefreshTransport → AuthTransport → debugTransport → base transport\n\t//\n\t// AuthTransport reads the current token from a shared TokenSource on\n\t// every request, so after a refresh all subsequent requests (REST and\n\t// GraphQL) immediately use the new token — no stale cached values.\n\ttransport := http.RoundTripper(http.DefaultTransport)\n\tif cfg.transport != nil {\n\t\ttransport = cfg.transport\n\t}\n\tif cfg.debug {\n\t\ttransport = &debugTransport{transport: transport}\n\t}\n\n\ttokenSource := bkhttp.NewTokenSource(token)\n\n\ttransport = &bkhttp.AuthTransport{\n\t\tBase:        transport,\n\t\tTokenSource: tokenSource,\n\t\tUserAgent:   userAgent,\n\t}\n\n\t// Add refresh transport if a refresh token is available for this org.\n\torg := conf.OrganizationSlug()\n\tif cfg.orgOverride != \"\" {\n\t\torg = cfg.orgOverride\n\t}\n\n\tkr := keyring.New()\n\tif refreshToken, err := kr.GetRefreshToken(org); err == nil && refreshToken != \"\" {\n\t\ttransport = &bkhttp.RefreshTransport{\n\t\t\tBase:        transport,\n\t\t\tOrg:         org,\n\t\t\tKeyring:     kr,\n\t\t\tTokenSource: tokenSource,\n\t\t}\n\t}\n\n\thttpClient := &http.Client{Transport: transport}\n\n\t// go-buildkite still needs WithTokenAuth to satisfy its constructor\n\t// requirement, but our AuthTransport is the canonical source of the\n\t// Authorization header.\n\tclientOpts := []buildkite.ClientOpt{\n\t\tbuildkite.WithBaseURL(conf.RESTAPIEndpoint()),\n\t\tbuildkite.WithTokenAuth(token),\n\t\tbuildkite.WithUserAgent(userAgent),\n\t\tbuildkite.WithHTTPClient(httpClient),\n\t}\n\n\tbuildkiteClient, err := buildkite.NewOpts(clientOpts...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating buildkite client: %w\", err)\n\t}\n\n\tgraphqlHTTPClient := &gqlHTTPClient{client: httpClient}\n\n\treturn &Factory{\n\t\tConfig:        conf,\n\t\tGitRepository: repo,\n\t\tGraphQLClient: graphql.NewClient(conf.GetGraphQLEndpoint(), graphqlHTTPClient),\n\t\tRestAPIClient: buildkiteClient,\n\t\tVersion:       version.Version,\n\t\tNoPager:       conf.PagerDisabled(),\n\t\tQuiet:         conf.Quiet(),\n\t\tNoInput:       conf.NoInput(),\n\t\tDebug:         cfg.debug,\n\t}, nil\n}\n"
  },
  {
    "path": "pkg/cmd/factory/factory_test.go",
    "content": "package factory\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\tbuildkite \"github.com/buildkite/go-buildkite/v4\"\n)\n\nfunc TestRedactHeaders(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\theader   string\n\t\tvalue    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"Bearer token\",\n\t\t\theader:   \"Authorization\",\n\t\t\tvalue:    \"Bearer bkua_1234567890abcdef\",\n\t\t\texpected: \"Bearer [REDACTED]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Basic auth\",\n\t\t\theader:   \"Authorization\",\n\t\t\tvalue:    \"Basic dXNlcjpwYXNz\",\n\t\t\texpected: \"Basic [REDACTED]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Token without type\",\n\t\t\theader:   \"Authorization\",\n\t\t\tvalue:    \"sometoken123\",\n\t\t\texpected: \"[REDACTED]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Non-sensitive header unchanged\",\n\t\t\theader:   \"Content-Type\",\n\t\t\tvalue:    \"application/json\",\n\t\t\texpected: \"application/json\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\theaders := http.Header{}\n\t\t\theaders.Set(tt.header, tt.value)\n\n\t\t\tredactHeaders(headers)\n\n\t\t\tgot := headers.Get(tt.header)\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"redactHeaders() = %q, want %q\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRedactHeadersMultipleValues(t *testing.T) {\n\theaders := http.Header{}\n\theaders.Add(\"Authorization\", \"Bearer token1\")\n\theaders.Add(\"Authorization\", \"Bearer token2\")\n\n\tredactHeaders(headers)\n\n\tvalues := headers.Values(\"Authorization\")\n\tif len(values) != 2 {\n\t\tt.Fatalf(\"expected 2 values, got %d\", len(values))\n\t}\n\n\tfor _, v := range values {\n\t\tif v != \"Bearer [REDACTED]\" {\n\t\t\tt.Errorf(\"expected 'Bearer [REDACTED]', got %q\", v)\n\t\t}\n\t}\n}\n\nfunc TestDebugTransportPreservesRequestBody(t *testing.T) {\n\texpectedBody := `{\"name\":\"test-pipeline\",\"cluster_id\":\"\",\"repository\":\"git@github.com:test/repo.git\"}`\n\n\t// Create a test server that checks the request body.\n\t// Note: the handler runs in a separate goroutine, so we capture errors\n\t// in a variable rather than calling t.Fatalf (which would hang the test).\n\tvar receivedBody string\n\tvar handlerErr error\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tbody, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\thandlerErr = err\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\treceivedBody = string(body)\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write([]byte(`{\"ok\": true}`))\n\t}))\n\tdefer server.Close()\n\n\t// Use the debug transport\n\tdt := &debugTransport{\n\t\ttransport: http.DefaultTransport,\n\t}\n\n\treq, err := http.NewRequest(\"POST\", server.URL, strings.NewReader(expectedBody))\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer test-token\")\n\n\tresp, err := dt.RoundTrip(req)\n\tif err != nil {\n\t\tt.Fatalf(\"RoundTrip failed: %v\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif handlerErr != nil {\n\t\tt.Fatalf(\"handler failed to read request body: %v\", handlerErr)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Errorf(\"expected status 200, got %d\", resp.StatusCode)\n\t}\n\n\tif receivedBody != expectedBody {\n\t\tt.Errorf(\"request body was not preserved through debug transport\\ngot:  %q\\nwant: %q\", receivedBody, expectedBody)\n\t}\n}\n\nfunc TestDebugTransportHandlesNilBody(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer server.Close()\n\n\tdt := &debugTransport{\n\t\ttransport: http.DefaultTransport,\n\t}\n\n\treq, err := http.NewRequest(\"GET\", server.URL, nil)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to create request: %v\", err)\n\t}\n\n\tresp, err := dt.RoundTrip(req)\n\tif err != nil {\n\t\tt.Fatalf(\"RoundTrip failed: %v\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Errorf(\"expected status 200, got %d\", resp.StatusCode)\n\t}\n}\n\nfunc TestBuildUserAgent(t *testing.T) {\n\tt.Run(\"default user agent has no preflight suffix\", func(t *testing.T) {\n\t\tgot := buildUserAgent(\"\")\n\t\tif !strings.Contains(got, buildkite.DefaultUserAgent) {\n\t\t\tt.Fatalf(\"expected default user agent %q in %q\", buildkite.DefaultUserAgent, got)\n\t\t}\n\t\tif strings.Contains(got, \"buildkite-cli-preflight/\") {\n\t\t\tt.Fatalf(\"expected no preflight suffix in %q\", got)\n\t\t}\n\t})\n\n\tt.Run(\"preflight suffix is appended when requested\", func(t *testing.T) {\n\t\tgot := buildUserAgent(\"buildkite-cli-preflight/3.x\")\n\t\tif !strings.Contains(got, buildkite.DefaultUserAgent) {\n\t\t\tt.Fatalf(\"expected default user agent %q in %q\", buildkite.DefaultUserAgent, got)\n\t\t}\n\t\tif !strings.Contains(got, \"buildkite-cli-preflight/3.x\") {\n\t\t\tt.Fatalf(\"expected preflight suffix in %q\", got)\n\t\t}\n\t})\n}\n\nfunc TestNewUserAgent(t *testing.T) {\n\tt.Chdir(t.TempDir())\n\n\tt.Run(\"non-preflight factory does not set preflight suffix\", func(t *testing.T) {\n\t\tf, err := New()\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"New() error = %v\", err)\n\t\t}\n\t\tif strings.Contains(f.RestAPIClient.UserAgent, \"buildkite-cli-preflight/\") {\n\t\t\tt.Fatalf(\"expected no preflight suffix in %q\", f.RestAPIClient.UserAgent)\n\t\t}\n\t})\n\n\tt.Run(\"factory can opt in to preflight suffix\", func(t *testing.T) {\n\t\tf, err := New(WithUserAgentSuffix(\"buildkite-cli-preflight/3.x\"))\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"New() error = %v\", err)\n\t\t}\n\t\tif !strings.Contains(f.RestAPIClient.UserAgent, \"buildkite-cli-preflight/3.x\") {\n\t\t\tt.Fatalf(\"expected preflight suffix in %q\", f.RestAPIClient.UserAgent)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "pkg/cmd/validation/config.go",
    "content": "package validation\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n)\n\n// CommandsNotRequiringToken is a list of command paths that don't require an API token\nvar CommandsNotRequiringToken = []string{\n\t\"pipeline validate\", // The pipeline validate command doesn't require an API token\n\t\"pipeline migrate\",  // The pipeline migrate command uses a public migration API\n}\n\n// ValidateConfiguration checks that the configuration is valid to execute the command (Kong version)\nfunc ValidateConfiguration(conf *config.Config, commandPath string) error {\n\treturn validateConfiguration(conf, commandPath, \"\")\n}\n\n// ValidateConfigurationForOrg checks configuration for a specific organization\n// context when a command supports --org.\nfunc ValidateConfigurationForOrg(conf *config.Config, commandPath, org string) error {\n\treturn validateConfiguration(conf, commandPath, org)\n}\n\nfunc validateConfiguration(conf *config.Config, commandPath, orgOverride string) error {\n\torg := conf.OrganizationSlug()\n\ttoken := conf.APIToken()\n\tif orgOverride != \"\" {\n\t\torg = orgOverride\n\t\tif t := conf.APITokenForOrg(org); t != \"\" {\n\t\t\ttoken = t\n\t\t}\n\t}\n\n\tmissingToken := token == \"\"\n\tmissingOrg := org == \"\"\n\n\t// Skip token check for all configure commands\n\tif strings.HasPrefix(commandPath, \"configure\") {\n\t\treturn nil\n\t}\n\n\t// Skip token check for commands that don't need it\n\tfor _, exemptCmd := range CommandsNotRequiringToken {\n\t\t// Check if the command path ends with the exempt command pattern\n\t\tif strings.HasSuffix(commandPath, exemptCmd) {\n\t\t\treturn nil // Skip validation for exempt commands\n\t\t}\n\t}\n\n\tswitch {\n\tcase missingToken && missingOrg:\n\t\treturn errors.New(\"you are not authenticated. Run bk auth login to authenticate, or run bk use to select a configured organization\")\n\tcase missingToken:\n\t\treturn errors.New(\"you are not authenticated. Run bk auth login to authenticate\")\n\t// an organization may not be present if the user is only viewing public resources\n\tcase missingOrg:\n\t\tfmt.Fprintln(os.Stderr, \"Warning: no organization set, only public pipelines will be visible. Run bk auth login, or bk use, to set an organization\")\n\t\treturn nil\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/cmd/validation/config_test.go",
    "content": "package validation\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/buildkite/cli/v3/internal/config\"\n\tbkKeyring \"github.com/buildkite/cli/v3/pkg/keyring\"\n)\n\nfunc TestValidateConfiguration_ExemptCommands(t *testing.T) {\n\tt.Setenv(\"BUILDKITE_API_TOKEN\", \"\")\n\tt.Setenv(\"BUILDKITE_ORGANIZATION_SLUG\", \"\")\n\tconf := newTestConfig(t)\n\n\tfor _, path := range []string{\n\t\t\"pipeline validate\",\n\t\t\"pipeline migrate\",\n\t\t\"configure\",\n\t\t\"configure default\",\n\t\t\"configure add\",\n\t} {\n\t\tif err := ValidateConfiguration(conf, path); err != nil {\n\t\t\tt.Fatalf(\"expected no error for exempt command %q, got %v\", path, err)\n\t\t}\n\t}\n}\n\nfunc TestValidateConfiguration_MissingValues(t *testing.T) {\n\tt.Run(\"missing token and org\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_API_TOKEN\", \"\")\n\t\tt.Setenv(\"BUILDKITE_ORGANIZATION_SLUG\", \"\")\n\t\tconf := newTestConfig(t)\n\t\tif err := ValidateConfiguration(conf, \"pipeline view\"); err == nil {\n\t\t\tt.Fatalf(\"expected error when token and org are missing\")\n\t\t}\n\t})\n\n\tt.Run(\"missing token\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_API_TOKEN\", \"\")\n\t\tt.Setenv(\"BUILDKITE_ORGANIZATION_SLUG\", \"org\")\n\t\tconf := newTestConfig(t)\n\t\tif err := ValidateConfiguration(conf, \"pipeline view\"); err == nil {\n\t\t\tt.Fatalf(\"expected error when token is missing\")\n\t\t}\n\t})\n\n\tt.Run(\"token and org present\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_API_TOKEN\", \"token2\")\n\t\tt.Setenv(\"BUILDKITE_ORGANIZATION_SLUG\", \"org2\")\n\t\tconf := newTestConfig(t)\n\t\tif err := ValidateConfiguration(conf, \"pipeline view\"); err != nil {\n\t\t\tt.Fatalf(\"expected no error when token and org are set, got %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"missing org warning is written to stderr\", func(t *testing.T) {\n\t\tt.Setenv(\"BUILDKITE_API_TOKEN\", \"token\")\n\t\tt.Setenv(\"BUILDKITE_ORGANIZATION_SLUG\", \"\")\n\t\tconf := newTestConfig(t)\n\n\t\tvar validationErr error\n\t\tstdout, stderr := captureStandardStreams(t, func() {\n\t\t\tvalidationErr = ValidateConfiguration(conf, \"pipeline view\")\n\t\t})\n\n\t\tif validationErr != nil {\n\t\t\tt.Fatalf(\"expected no error when only org is missing, got %v\", validationErr)\n\t\t}\n\n\t\tif stdout != \"\" {\n\t\t\tt.Fatalf(\"expected stdout to remain empty, got %q\", stdout)\n\t\t}\n\n\t\tif !strings.Contains(stderr, \"Warning: no organization set\") {\n\t\t\tt.Fatalf(\"expected stderr warning, got %q\", stderr)\n\t\t}\n\t})\n}\n\nfunc newTestConfig(t *testing.T) *config.Config {\n\tt.Helper()\n\tt.Setenv(\"HOME\", t.TempDir())\n\tt.Setenv(\"XDG_CONFIG_HOME\", \"\")\n\tbkKeyring.MockForTesting()\n\treturn config.New(nil, nil)\n}\n\nfunc captureStandardStreams(t *testing.T, fn func()) (stdout, stderr string) {\n\tt.Helper()\n\n\toldStdout := os.Stdout\n\toldStderr := os.Stderr\n\n\tstdoutR, stdoutW, err := os.Pipe()\n\tif err != nil {\n\t\tt.Fatalf(\"os.Pipe() stdout error = %v\", err)\n\t}\n\tstderrR, stderrW, err := os.Pipe()\n\tif err != nil {\n\t\tt.Fatalf(\"os.Pipe() stderr error = %v\", err)\n\t}\n\n\tos.Stdout = stdoutW\n\tos.Stderr = stderrW\n\n\tdefer func() {\n\t\tos.Stdout = oldStdout\n\t\tos.Stderr = oldStderr\n\t}()\n\n\tfn()\n\n\tif err := stdoutW.Close(); err != nil {\n\t\tt.Fatalf(\"stdout close error = %v\", err)\n\t}\n\tif err := stderrW.Close(); err != nil {\n\t\tt.Fatalf(\"stderr close error = %v\", err)\n\t}\n\n\tstdoutBytes, err := io.ReadAll(stdoutR)\n\tif err != nil {\n\t\tt.Fatalf(\"stdout read error = %v\", err)\n\t}\n\tstderrBytes, err := io.ReadAll(stderrR)\n\tif err != nil {\n\t\tt.Fatalf(\"stderr read error = %v\", err)\n\t}\n\n\tif err := stdoutR.Close(); err != nil {\n\t\tt.Fatalf(\"stdout reader close error = %v\", err)\n\t}\n\tif err := stderrR.Close(); err != nil {\n\t\tt.Fatalf(\"stderr reader close error = %v\", err)\n\t}\n\n\treturn string(stdoutBytes), string(stderrBytes)\n}\n"
  },
  {
    "path": "pkg/keyring/keyring.go",
    "content": "// Package keyring provides secure credential storage using the OS keychain.\n// It falls back to file-based storage when the keychain is unavailable (e.g., in CI environments).\npackage keyring\n\nimport (\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/zalando/go-keyring\"\n)\n\nconst (\n\tserviceName        = \"buildkite-cli\"\n\trefreshServiceName = \"buildkite-cli-refresh\"\n)\n\nvar (\n\tkeyringAvailableOnce sync.Once\n\tkeyringAvailable     bool\n)\n\n// Keyring provides secure credential storage with fallback support\ntype Keyring struct {\n\tuseKeyring bool\n}\n\n// New creates a new Keyring instance.\n// It automatically detects if the system keyring is available.\nfunc New() *Keyring {\n\treturn &Keyring{\n\t\tuseKeyring: isKeyringAvailable(),\n\t}\n}\n\n// Set stores a token for the given organization\nfunc (k *Keyring) Set(org, token string) error {\n\tif !k.useKeyring {\n\t\treturn nil // Fallback handled by config file\n\t}\n\treturn keyring.Set(serviceName, org, token)\n}\n\n// Get retrieves a token for the given organization\nfunc (k *Keyring) Get(org string) (string, error) {\n\tif !k.useKeyring {\n\t\treturn \"\", keyring.ErrNotFound\n\t}\n\treturn keyring.Get(serviceName, org)\n}\n\n// Delete removes a token for the given organization\nfunc (k *Keyring) Delete(org string) error {\n\tif !k.useKeyring {\n\t\treturn nil\n\t}\n\treturn keyring.Delete(serviceName, org)\n}\n\n// SetRefreshToken stores a refresh token for the given organization\nfunc (k *Keyring) SetRefreshToken(org, token string) error {\n\tif !k.useKeyring {\n\t\treturn nil\n\t}\n\treturn keyring.Set(refreshServiceName, org, token)\n}\n\n// GetRefreshToken retrieves a refresh token for the given organization\nfunc (k *Keyring) GetRefreshToken(org string) (string, error) {\n\tif !k.useKeyring {\n\t\treturn \"\", keyring.ErrNotFound\n\t}\n\treturn keyring.Get(refreshServiceName, org)\n}\n\n// DeleteRefreshToken removes a refresh token for the given organization\nfunc (k *Keyring) DeleteRefreshToken(org string) error {\n\tif !k.useKeyring {\n\t\treturn nil\n\t}\n\treturn keyring.Delete(refreshServiceName, org)\n}\n\n// IsAvailable returns true if the system keyring is available\nfunc (k *Keyring) IsAvailable() bool {\n\treturn k.useKeyring\n}\n\n// MockForTesting replaces the keyring backend with an in-memory store\n// and marks it as available so subsequent New() calls use the mock.\nfunc MockForTesting() {\n\tkeyring.MockInit()\n\tkeyringAvailableOnce = sync.Once{}\n\tkeyringAvailableOnce.Do(func() {\n\t\tkeyringAvailable = true\n\t})\n}\n\n// ResetForTesting resets the availability cache so that the next call to\n// New() re-evaluates the environment. Intended for use in tests only.\nfunc ResetForTesting() {\n\tkeyringAvailableOnce = sync.Once{}\n\tkeyringAvailable = false\n}\n\n// isKeyringAvailable checks if the system keyring can be used\nfunc isKeyringAvailable() bool {\n\tkeyringAvailableOnce.Do(func() {\n\t\t// Disable keyring if explicitly opted out\n\t\tif os.Getenv(\"BUILDKITE_NO_KEYRING\") != \"\" {\n\t\t\tkeyringAvailable = false\n\t\t\treturn\n\t\t}\n\n\t\t// Disable keyring in CI environments\n\t\tif os.Getenv(\"CI\") != \"\" || os.Getenv(\"BUILDKITE\") != \"\" {\n\t\t\tkeyringAvailable = false\n\t\t\treturn\n\t\t}\n\n\t\t// Assume keyring is available; callers can handle errors\n\t\tkeyringAvailable = true\n\t})\n\treturn keyringAvailable\n}\n"
  },
  {
    "path": "pkg/keyring/keyring_test.go",
    "content": "package keyring\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\n// setEnv sets an environment variable for the duration of the test and\n// restores the original value (or unsets it) via t.Cleanup.\nfunc setEnv(t *testing.T, key, value string) {\n\tt.Helper()\n\toriginal, had := os.LookupEnv(key)\n\tif err := os.Setenv(key, value); err != nil {\n\t\tt.Fatalf(\"failed to set env %s: %v\", key, err)\n\t}\n\tt.Cleanup(func() {\n\t\tif had {\n\t\t\tos.Setenv(key, original)\n\t\t} else {\n\t\t\tos.Unsetenv(key)\n\t\t}\n\t\t// Reset the once so the next test starts fresh.\n\t\tResetForTesting()\n\t})\n\t// Reset now so this test sees the new env value.\n\tResetForTesting()\n}\n\nfunc TestIsKeyringAvailable(t *testing.T) {\n\t// These tests manipulate package-level state (sync.Once) so must not run\n\t// in parallel with each other.\n\n\tt.Run(\"disabled by BUILDKITE_NO_KEYRING\", func(t *testing.T) {\n\t\tsetEnv(t, \"BUILDKITE_NO_KEYRING\", \"1\")\n\t\tsetEnv(t, \"CI\", \"\")\n\t\tsetEnv(t, \"BUILDKITE\", \"\")\n\n\t\tkr := New()\n\t\tif kr.IsAvailable() {\n\t\t\tt.Error(\"expected keyring to be unavailable when BUILDKITE_NO_KEYRING is set\")\n\t\t}\n\t})\n\n\tt.Run(\"disabled by CI\", func(t *testing.T) {\n\t\tsetEnv(t, \"CI\", \"true\")\n\t\tsetEnv(t, \"BUILDKITE_NO_KEYRING\", \"\")\n\t\tsetEnv(t, \"BUILDKITE\", \"\")\n\n\t\tkr := New()\n\t\tif kr.IsAvailable() {\n\t\t\tt.Error(\"expected keyring to be unavailable when CI is set\")\n\t\t}\n\t})\n\n\tt.Run(\"disabled by BUILDKITE\", func(t *testing.T) {\n\t\tsetEnv(t, \"BUILDKITE\", \"true\")\n\t\tsetEnv(t, \"BUILDKITE_NO_KEYRING\", \"\")\n\t\tsetEnv(t, \"CI\", \"\")\n\n\t\tkr := New()\n\t\tif kr.IsAvailable() {\n\t\t\tt.Error(\"expected keyring to be unavailable when BUILDKITE is set\")\n\t\t}\n\t})\n}\n\nfunc TestNoKeyringGet(t *testing.T) {\n\tsetEnv(t, \"BUILDKITE_NO_KEYRING\", \"1\")\n\tsetEnv(t, \"CI\", \"\")\n\tsetEnv(t, \"BUILDKITE\", \"\")\n\n\tkr := New()\n\ttoken, err := kr.Get(\"my-org\")\n\tif token != \"\" {\n\t\tt.Errorf(\"Get() returned non-empty token with keyring disabled, got %q\", token)\n\t}\n\tif err == nil {\n\t\tt.Error(\"Get() expected ErrNotFound when keyring is disabled, got nil\")\n\t}\n}\n\nfunc TestNoKeyringSet(t *testing.T) {\n\tsetEnv(t, \"BUILDKITE_NO_KEYRING\", \"1\")\n\tsetEnv(t, \"CI\", \"\")\n\tsetEnv(t, \"BUILDKITE\", \"\")\n\n\tkr := New()\n\tif err := kr.Set(\"my-org\", \"token-123\"); err != nil {\n\t\tt.Errorf(\"Set() returned unexpected error with keyring disabled: %v\", err)\n\t}\n}\n\nfunc TestNoKeyringDelete(t *testing.T) {\n\tsetEnv(t, \"BUILDKITE_NO_KEYRING\", \"1\")\n\tsetEnv(t, \"CI\", \"\")\n\tsetEnv(t, \"BUILDKITE\", \"\")\n\n\tkr := New()\n\tif err := kr.Delete(\"my-org\"); err != nil {\n\t\tt.Errorf(\"Delete() returned unexpected error with keyring disabled: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "pkg/oauth/oauth.go",
    "content": "// Package oauth provides OAuth 2.0 PKCE authentication flow for CLI applications\npackage oauth\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst (\n\tDefaultHost = \"buildkite.com\"\n)\n\n// AllScopes is the complete set of Buildkite API token scopes. When no --scopes\n// flag is provided, the CLI requests all of these and Buildkite grants only the\n// ones the user actually has permission for.\n//\n// Reference: https://buildkite.com/docs/apis/managing-api-tokens\nvar AllScopes = []string{\n\t// CI/CD\n\t\"read_agents\",\n\t\"read_artifacts\",\n\t\"read_build_logs\",\n\t\"read_builds\",\n\t\"read_clusters\",\n\t\"read_job_env\",\n\t\"read_pipeline_templates\",\n\t\"read_pipelines\",\n\t\"read_rules\",\n\t\"write_agents\",\n\t\"write_artifacts\",\n\t\"write_build_logs\",\n\t\"write_builds\",\n\t\"write_clusters\",\n\t\"write_pipeline_templates\",\n\t\"write_pipelines\",\n\t\"write_rules\",\n\n\t// Organization and Users\n\t\"read_organizations\",\n\t\"read_teams\",\n\t\"read_user\",\n\t\"write_teams\",\n\n\t// Security\n\t\"read_secrets_details\",\n\t\"write_secrets\",\n\n\t// Test Engine\n\t\"read_suites\",\n\t\"read_test_plan\",\n\t\"write_suites\",\n\t\"write_test_plan\",\n\n\t// Packages\n\t\"delete_packages\",\n\t\"delete_registries\",\n\t\"read_packages\",\n\t\"read_registries\",\n\t\"write_packages\",\n\t\"write_registries\",\n\n\t// Portals\n\t\"read_portals\",\n\t\"write_portals\",\n}\n\n// ScopeGroups defines named groups of scopes that can be used with --scopes.\n// For example, --scopes \"read_only\" expands to all read_* scopes.\nvar ScopeGroups = map[string][]string{\n\t\"read_only\": {\n\t\t\"read_agents\",\n\t\t\"read_artifacts\",\n\t\t\"read_build_logs\",\n\t\t\"read_builds\",\n\t\t\"read_clusters\",\n\t\t\"read_job_env\",\n\t\t\"read_organizations\",\n\t\t\"read_packages\",\n\t\t\"read_pipeline_templates\",\n\t\t\"read_pipelines\",\n\t\t\"read_portals\",\n\t\t\"read_registries\",\n\t\t\"read_rules\",\n\t\t\"read_secrets_details\",\n\t\t\"read_suites\",\n\t\t\"read_teams\",\n\t\t\"read_test_plan\",\n\t\t\"read_user\",\n\t},\n}\n\n// ResolveScopes expands scope group names (e.g., \"read_only\") into their\n// individual scopes. Unknown tokens are passed through as literal scopes.\n// Multiple groups and individual scopes can be mixed:\n//\n//\t\"read_only write_builds\" → \"read_agents read_artifacts ... write_builds\"\nfunc ResolveScopes(input string) string {\n\tif input == \"\" {\n\t\treturn \"\"\n\t}\n\n\tseen := make(map[string]bool)\n\tvar resolved []string\n\n\tfor _, token := range strings.Fields(input) {\n\t\tif group, ok := ScopeGroups[token]; ok {\n\t\t\tfor _, s := range group {\n\t\t\t\tif !seen[s] {\n\t\t\t\t\tseen[s] = true\n\t\t\t\t\tresolved = append(resolved, s)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif !seen[token] {\n\t\t\t\tseen[token] = true\n\t\t\t\tresolved = append(resolved, token)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.Join(resolved, \" \")\n}\n\n// DefaultClientID is the OAuth client ID for the Buildkite CLI\n// This can be overridden with ldflags\nvar DefaultClientID = \"5214b230f06b48938ab5\"\n\n// Config holds OAuth configuration\ntype Config struct {\n\tHost        string // e.g., \"buildkite.com\"\n\tClientID    string // OAuth client ID\n\tOrgSlug     string // Organization slug to request access for\n\tOrgUUID     string // Organization UUID to request access for\n\tCallbackURL string // e.g., \"http://127.0.0.1:8080/callback\"\n\tScopes      string // Space-separated OAuth scopes\n}\n\n// CallbackResult holds the result from the OAuth callback\ntype CallbackResult struct {\n\tCode  string\n\tState string\n\tError string\n}\n\n// TokenResponse holds the token exchange response\ntype TokenResponse struct {\n\tAccessToken  string `json:\"access_token\"`\n\tTokenType    string `json:\"token_type\"`\n\tScope        string `json:\"scope\"`\n\tRefreshToken string `json:\"refresh_token,omitempty\"`\n\tExpiresIn    int    `json:\"expires_in,omitempty\"`\n\tError        string `json:\"error,omitempty\"`\n\tErrorDesc    string `json:\"error_description,omitempty\"`\n}\n\n// Flow manages an OAuth authentication flow\ntype Flow struct {\n\tconfig       *Config\n\tcodeVerifier string\n\tstate        string\n\tlistener     net.Listener\n}\n\n// NewFlow creates a new OAuth flow\nfunc NewFlow(cfg *Config) (*Flow, error) {\n\tif cfg.Host == \"\" {\n\t\t// Allow override via environment variable for local development\n\t\tif envHost := os.Getenv(\"BUILDKITE_HOST\"); envHost != \"\" {\n\t\t\tcfg.Host = envHost\n\t\t} else {\n\t\t\tcfg.Host = DefaultHost\n\t\t}\n\t}\n\tif cfg.ClientID == \"\" {\n\t\tcfg.ClientID = DefaultClientID\n\t}\n\tif cfg.Scopes == \"\" {\n\t\tcfg.Scopes = strings.Join(AllScopes, \" \")\n\t}\n\n\t// Generate PKCE verifier and state\n\tcodeVerifier, err := generateCodeVerifier()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to generate code verifier: %w\", err)\n\t}\n\n\tstate, err := generateState()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to generate state: %w\", err)\n\t}\n\n\tvar listener net.Listener\n\n\t// Only start local callback server if no custom redirect URI provided\n\tif cfg.CallbackURL == \"\" {\n\t\tvar err error\n\t\tlistener, err = net.Listen(\"tcp\", \"127.0.0.1:0\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to start callback server: %w\", err)\n\t\t}\n\t\tcfg.CallbackURL = fmt.Sprintf(\"http://127.0.0.1:%d/callback\", listener.Addr().(*net.TCPAddr).Port)\n\t}\n\n\treturn &Flow{\n\t\tconfig:       cfg,\n\t\tcodeVerifier: codeVerifier,\n\t\tstate:        state,\n\t\tlistener:     listener,\n\t}, nil\n}\n\n// AuthorizationURL returns the URL to open in the browser\nfunc (f *Flow) AuthorizationURL() string {\n\tcodeChallenge := generateCodeChallenge(f.codeVerifier)\n\n\tparams := url.Values{\n\t\t\"client_id\":             {f.config.ClientID},\n\t\t\"response_type\":         {\"code\"},\n\t\t\"scope\":                 {f.config.Scopes},\n\t\t\"redirect_uri\":          {f.config.CallbackURL},\n\t\t\"state\":                 {f.state},\n\t\t\"code_challenge\":        {codeChallenge},\n\t\t\"code_challenge_method\": {\"S256\"},\n\t}\n\n\tif f.config.OrgUUID != \"\" {\n\t\tparams.Set(\"organization_uuid\", f.config.OrgUUID)\n\t} else if f.config.OrgSlug != \"\" {\n\t\tparams.Set(\"organization\", f.config.OrgSlug)\n\t}\n\n\treturn fmt.Sprintf(\"https://%s/oauth/authorize?%s\", f.config.Host, params.Encode())\n}\n\n// WaitForCallback waits for the OAuth callback and returns the authorization code\nfunc (f *Flow) WaitForCallback(ctx context.Context) (*CallbackResult, error) {\n\tif f.listener == nil {\n\t\treturn nil, fmt.Errorf(\"callback listener not available as a custom CallbackURL was provided\")\n\t}\n\n\tresultCh := make(chan *CallbackResult, 1)\n\terrCh := make(chan error, 1)\n\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/callback\", func(w http.ResponseWriter, r *http.Request) {\n\t\tcode := r.URL.Query().Get(\"code\")\n\t\tstate := r.URL.Query().Get(\"state\")\n\t\terrMsg := r.URL.Query().Get(\"error\")\n\n\t\tresult := &CallbackResult{\n\t\t\tCode:  code,\n\t\t\tState: state,\n\t\t\tError: errMsg,\n\t\t}\n\n\t\t// Validate state\n\t\tif state != f.state {\n\t\t\tresult.Error = \"state mismatch - possible CSRF attack\"\n\t\t}\n\n\t\t// Send response to browser\n\t\tw.Header().Set(\"Content-Type\", \"text/html\")\n\t\tif result.Error == \"\" && result.Code != \"\" {\n\t\t\t// This is the page which lets folks know if they have been auth'd etc\n\t\t\t// then that they can close the window, I tried adding an emoji in here\n\t\t\t// but it renders weird\n\t\t\tfmt.Fprint(w, `<!DOCTYPE html>\n<html>\n<head><title>Authentication Successful</title></head>\n<body style=\"font-family: system-ui, sans-serif; text-align: center; padding: 50px;\">\n<h1>&#10003; Authentication Successful</h1>\n<p>You can close this window and return to your terminal.</p>\n</body>\n</html>`)\n\t\t} else {\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\tfmt.Fprintf(w, `<!DOCTYPE html>\n<html>\n<head><title>Authentication Failed</title></head>\n<body style=\"font-family: system-ui, sans-serif; text-align: center; padding: 50px;\">\n<h1>&#10005; Authentication Failed</h1>\n<p>Error: %s</p>\n</body>\n</html>`, result.Error)\n\t\t}\n\n\t\tresultCh <- result\n\t})\n\n\tserver := &http.Server{Handler: mux}\n\tgo func() {\n\t\tif err := server.Serve(f.listener); err != http.ErrServerClosed {\n\t\t\terrCh <- err\n\t\t}\n\t}()\n\n\tdefer func() {\n\t\t_ = server.Shutdown(context.Background())\n\t}()\n\n\tselect {\n\tcase result := <-resultCh:\n\t\tif result.Error != \"\" {\n\t\t\treturn nil, fmt.Errorf(\"authorization failed: %s\", result.Error)\n\t\t}\n\t\treturn result, nil\n\tcase err := <-errCh:\n\t\treturn nil, fmt.Errorf(\"callback server error: %w\", err)\n\tcase <-ctx.Done():\n\t\treturn nil, ctx.Err()\n\t}\n}\n\n// ExchangeCode exchanges the authorization code for an access token\nfunc (f *Flow) ExchangeCode(ctx context.Context, code string) (*TokenResponse, error) {\n\ttokenURL := fmt.Sprintf(\"https://%s/oauth/token\", f.config.Host)\n\n\tdata := url.Values{\n\t\t\"grant_type\":    {\"authorization_code\"},\n\t\t\"code\":          {code},\n\t\t\"client_id\":     {f.config.ClientID},\n\t\t\"redirect_uri\":  {f.config.CallbackURL},\n\t\t\"code_verifier\": {f.codeVerifier},\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", tokenURL, strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"token request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar tokenResp TokenResponse\n\tif err := json.Unmarshal(body, &tokenResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse token response: %w\", err)\n\t}\n\n\tif tokenResp.Error != \"\" {\n\t\treturn nil, fmt.Errorf(\"token error: %s - %s\", tokenResp.Error, tokenResp.ErrorDesc)\n\t}\n\n\tif tokenResp.AccessToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"no access token in response\")\n\t}\n\n\treturn &tokenResp, nil\n}\n\n// RefreshAccessToken exchanges a refresh token for a new access token and refresh token.\nfunc RefreshAccessToken(ctx context.Context, host, clientID, refreshToken string) (*TokenResponse, error) {\n\tif host == \"\" {\n\t\tif envHost := os.Getenv(\"BUILDKITE_HOST\"); envHost != \"\" {\n\t\t\thost = envHost\n\t\t} else {\n\t\t\thost = DefaultHost\n\t\t}\n\t}\n\tif clientID == \"\" {\n\t\tclientID = DefaultClientID\n\t}\n\n\ttokenURL := fmt.Sprintf(\"https://%s/oauth/token\", host)\n\n\tdata := url.Values{\n\t\t\"grant_type\":    {\"refresh_token\"},\n\t\t\"refresh_token\": {refreshToken},\n\t\t\"client_id\":     {clientID},\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", tokenURL, strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"Accept\", \"application/json\")\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"refresh token request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar tokenResp TokenResponse\n\tif err := json.Unmarshal(body, &tokenResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse token response: %w\", err)\n\t}\n\n\tif tokenResp.Error != \"\" {\n\t\treturn nil, fmt.Errorf(\"token refresh error: %s - %s\", tokenResp.Error, tokenResp.ErrorDesc)\n\t}\n\n\tif tokenResp.AccessToken == \"\" {\n\t\treturn nil, fmt.Errorf(\"no access token in refresh response\")\n\t}\n\n\treturn &tokenResp, nil\n}\n\n// Close cleans up the OAuth flow resources\nfunc (f *Flow) Close() error {\n\tif f.listener != nil {\n\t\treturn f.listener.Close()\n\t}\n\treturn nil\n}\n\n// generateCodeVerifier generates a PKCE code verifier\nfunc generateCodeVerifier() (string, error) {\n\tb := make([]byte, 32)\n\tif _, err := rand.Read(b); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn base64.RawURLEncoding.EncodeToString(b), nil\n}\n\n// generateCodeChallenge generates a PKCE code challenge from the verifier\nfunc generateCodeChallenge(verifier string) string {\n\thash := sha256.Sum256([]byte(verifier))\n\treturn base64.RawURLEncoding.EncodeToString(hash[:])\n}\n\n// generateState generates a random state parameter\nfunc generateState() (string, error) {\n\tb := make([]byte, 16)\n\tif _, err := rand.Read(b); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn base64.RawURLEncoding.EncodeToString(b), nil\n}\n"
  },
  {
    "path": "pkg/oauth/oauth_test.go",
    "content": "package oauth\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestResolveScopes(t *testing.T) {\n\tt.Parallel()\n\n\treadOnlyExpanded := strings.Join(ScopeGroups[\"read_only\"], \" \")\n\n\ttests := []struct {\n\t\tname  string\n\t\tinput string\n\t\twant  string\n\t}{\n\t\t{\n\t\t\tname:  \"empty returns empty\",\n\t\t\tinput: \"\",\n\t\t\twant:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:  \"individual scopes pass through\",\n\t\t\tinput: \"read_user write_builds\",\n\t\t\twant:  \"read_user write_builds\",\n\t\t},\n\t\t{\n\t\t\tname:  \"read_only group expands\",\n\t\t\tinput: \"read_only\",\n\t\t\twant:  readOnlyExpanded,\n\t\t},\n\t\t{\n\t\t\tname:  \"group mixed with individual scopes\",\n\t\t\tinput: \"read_only write_builds\",\n\t\t\twant:  readOnlyExpanded + \" write_builds\",\n\t\t},\n\t\t{\n\t\t\tname:  \"duplicate scopes are deduplicated\",\n\t\t\tinput: \"read_only read_user read_builds\",\n\t\t\twant:  readOnlyExpanded,\n\t\t},\n\t\t{\n\t\t\tname:  \"unknown group names pass through as literal scopes\",\n\t\t\tinput: \"custom_scope\",\n\t\t\twant:  \"custom_scope\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot := ResolveScopes(tt.input)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"ResolveScopes(%q)\\n  got:  %q\\n  want: %q\", tt.input, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewFlow_DefaultsToAllScopes(t *testing.T) {\n\tt.Parallel()\n\n\tflow, err := NewFlow(&Config{\n\t\tHost:        \"buildkite.com\",\n\t\tClientID:    \"test-client\",\n\t\tCallbackURL: \"http://localhost:9999/callback\",\n\t\tScopes:      \"\",\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"NewFlow: %v\", err)\n\t}\n\n\tauthURL := flow.AuthorizationURL()\n\tif !strings.Contains(authURL, \"scope=\") {\n\t\tt.Fatal(\"expected scope parameter in URL\")\n\t}\n\n\t// Verify all scopes are present\n\tfor _, s := range AllScopes {\n\t\tif !strings.Contains(authURL, s) {\n\t\t\tt.Errorf(\"expected scope %q in URL, got: %s\", s, authURL)\n\t\t}\n\t}\n}\n\nfunc TestAuthorizationURL_IncludesOrganizationSlug(t *testing.T) {\n\tt.Parallel()\n\n\tflow, err := NewFlow(&Config{\n\t\tHost:        \"buildkite.com\",\n\t\tClientID:    \"test-client\",\n\t\tCallbackURL: \"http://localhost:9999/callback\",\n\t\tOrgSlug:     \"buildkite\",\n\t\tScopes:      \"read_user\",\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"NewFlow: %v\", err)\n\t}\n\n\tauthURL, err := url.Parse(flow.AuthorizationURL())\n\tif err != nil {\n\t\tt.Fatalf(\"Parse AuthorizationURL: %v\", err)\n\t}\n\n\tquery := authURL.Query()\n\tif got := query.Get(\"organization\"); got != \"buildkite\" {\n\t\tt.Fatalf(\"organization = %q, want %q\", got, \"buildkite\")\n\t}\n\tif got := query.Get(\"organization_uuid\"); got != \"\" {\n\t\tt.Fatalf(\"organization_uuid = %q, want empty\", got)\n\t}\n}\n\nfunc TestAuthorizationURL_IncludesOrganizationUUID(t *testing.T) {\n\tt.Parallel()\n\n\tconst orgUUID = \"018f2f7e-7e99-7d77-b4d3-a95cb01805f4\"\n\n\tflow, err := NewFlow(&Config{\n\t\tHost:        \"buildkite.com\",\n\t\tClientID:    \"test-client\",\n\t\tCallbackURL: \"http://localhost:9999/callback\",\n\t\tOrgUUID:     orgUUID,\n\t\tScopes:      \"read_user\",\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"NewFlow: %v\", err)\n\t}\n\n\tauthURL, err := url.Parse(flow.AuthorizationURL())\n\tif err != nil {\n\t\tt.Fatalf(\"Parse AuthorizationURL: %v\", err)\n\t}\n\n\tquery := authURL.Query()\n\tif got := query.Get(\"organization_uuid\"); got != orgUUID {\n\t\tt.Fatalf(\"organization_uuid = %q, want %q\", got, orgUUID)\n\t}\n\tif got := query.Get(\"organization\"); got != \"\" {\n\t\tt.Fatalf(\"organization = %q, want empty\", got)\n\t}\n}\n\nfunc TestAuthorizationURL_UsesProvidedScopes(t *testing.T) {\n\tt.Parallel()\n\n\tflow, err := NewFlow(&Config{\n\t\tHost:        \"buildkite.com\",\n\t\tClientID:    \"test-client\",\n\t\tCallbackURL: \"http://localhost:9999/callback\",\n\t\tScopes:      \"read_user read_builds\",\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"NewFlow: %v\", err)\n\t}\n\n\tauthURL := flow.AuthorizationURL()\n\tif !strings.Contains(authURL, \"scope=\") {\n\t\tt.Fatal(\"expected scope parameter in URL\")\n\t}\n\t// Should use the provided scopes, not all scopes\n\tif strings.Contains(authURL, \"write_builds\") {\n\t\tt.Errorf(\"expected only provided scopes, but found write_builds in URL: %s\", authURL)\n\t}\n}\n"
  },
  {
    "path": "pkg/oauth/refresh_test.go",
    "content": "package oauth\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestRefreshAccessToken_Success(t *testing.T) {\n\tserver := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method != \"POST\" {\n\t\t\tt.Errorf(\"expected POST, got %s\", r.Method)\n\t\t}\n\t\tif r.URL.Path != \"/oauth/token\" {\n\t\t\tt.Errorf(\"expected /oauth/token, got %s\", r.URL.Path)\n\t\t}\n\n\t\tif err := r.ParseForm(); err != nil {\n\t\t\tt.Fatalf(\"failed to parse form: %v\", err)\n\t\t}\n\n\t\tif got := r.FormValue(\"grant_type\"); got != \"refresh_token\" {\n\t\t\tt.Errorf(\"expected grant_type=refresh_token, got %s\", got)\n\t\t}\n\t\tif got := r.FormValue(\"refresh_token\"); got != \"bkur_old_refresh_token\" {\n\t\t\tt.Errorf(\"expected refresh_token=bkur_old_refresh_token, got %s\", got)\n\t\t}\n\t\tif got := r.FormValue(\"client_id\"); got != \"test-client\" {\n\t\t\tt.Errorf(\"expected client_id=test-client, got %s\", got)\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.Write([]byte(`{\n\t\t\t\"access_token\": \"new_access_token\",\n\t\t\t\"token_type\": \"Bearer\",\n\t\t\t\"scope\": \"read_user read_organizations\",\n\t\t\t\"refresh_token\": \"bkur_new_refresh_token\",\n\t\t\t\"expires_in\": 3600\n\t\t}`))\n\t}))\n\tdefer server.Close()\n\n\t// Override the default HTTP client to trust the test server's TLS cert\n\torigTransport := http.DefaultTransport\n\thttp.DefaultTransport = server.Client().Transport\n\tdefer func() { http.DefaultTransport = origTransport }()\n\n\t// Extract host from the test server URL (strip https://)\n\thost := server.URL[len(\"https://\"):]\n\n\tresp, err := RefreshAccessToken(context.Background(), host, \"test-client\", \"bkur_old_refresh_token\")\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\tif resp.AccessToken != \"new_access_token\" {\n\t\tt.Errorf(\"expected access_token=new_access_token, got %s\", resp.AccessToken)\n\t}\n\tif resp.RefreshToken != \"bkur_new_refresh_token\" {\n\t\tt.Errorf(\"expected refresh_token=bkur_new_refresh_token, got %s\", resp.RefreshToken)\n\t}\n\tif resp.ExpiresIn != 3600 {\n\t\tt.Errorf(\"expected expires_in=3600, got %d\", resp.ExpiresIn)\n\t}\n\tif resp.Scope != \"read_user read_organizations\" {\n\t\tt.Errorf(\"expected scope=read_user read_organizations, got %s\", resp.Scope)\n\t}\n}\n\nfunc TestRefreshAccessToken_ErrorResponse(t *testing.T) {\n\tserver := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\tw.Write([]byte(`{\n\t\t\t\"error\": \"invalid_grant\",\n\t\t\t\"error_description\": \"Invalid refresh token\"\n\t\t}`))\n\t}))\n\tdefer server.Close()\n\n\torigTransport := http.DefaultTransport\n\thttp.DefaultTransport = server.Client().Transport\n\tdefer func() { http.DefaultTransport = origTransport }()\n\n\thost := server.URL[len(\"https://\"):]\n\n\t_, err := RefreshAccessToken(context.Background(), host, \"test-client\", \"bad-token\")\n\tif err == nil {\n\t\tt.Fatal(\"expected error, got nil\")\n\t}\n\n\texpected := \"token refresh error: invalid_grant - Invalid refresh token\"\n\tif err.Error() != expected {\n\t\tt.Errorf(\"expected error %q, got %q\", expected, err.Error())\n\t}\n}\n"
  },
  {
    "path": "pkg/output/color.go",
    "content": "package output\n\nimport (\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/mattn/go-isatty\"\n)\n\nvar (\n\tcolorOnce    sync.Once\n\tcolorEnabled = true\n)\n\n// ColorEnabled returns false when the NO_COLOR environment variable is present\n// See https://no-color.org for the convention\nfunc ColorEnabled() bool {\n\tcolorOnce.Do(func() {\n\t\tif _, disabled := os.LookupEnv(\"NO_COLOR\"); disabled {\n\t\t\tcolorEnabled = false\n\t\t\treturn\n\t\t}\n\n\t\tif term := os.Getenv(\"TERM\"); term == \"dumb\" {\n\t\t\tcolorEnabled = false\n\t\t\treturn\n\t\t}\n\n\t\tif !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) {\n\t\t\tcolorEnabled = false\n\t\t\treturn\n\t\t}\n\t})\n\treturn colorEnabled\n}\n"
  },
  {
    "path": "pkg/output/flags.go",
    "content": "package output\n\n// OutputFlags provides shorthand flags for output format selection.\n// Embed this struct in command structs to get --json, --yaml, --text flags\n// in addition to the existing --output/-o flag.\ntype OutputFlags struct {\n\tJSON   bool   `help:\"Output as JSON\" xor:\"format\"`\n\tYAML   bool   `help:\"Output as YAML\" xor:\"format\"`\n\tText   bool   `help:\"Output as text\" xor:\"format\"`\n\tOutput string `help:\"Output format. One of: json, yaml, text\" short:\"o\" default:\"${output_default_format}\" enum:\",json,yaml,text\"`\n}\n\n// AfterApply is called by Kong after parsing to map boolean flags to the Output string.\nfunc (o *OutputFlags) AfterApply() error {\n\tswitch {\n\tcase o.JSON:\n\t\to.Output = \"json\"\n\tcase o.YAML:\n\t\to.Output = \"yaml\"\n\tcase o.Text:\n\t\to.Output = \"text\"\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/output/flags_test.go",
    "content": "package output\n\nimport \"testing\"\n\nfunc TestOutputFlags_AfterApply(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tflags    OutputFlags\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"json flag sets output to json\",\n\t\t\tflags:    OutputFlags{JSON: true},\n\t\t\texpected: \"json\",\n\t\t},\n\t\t{\n\t\t\tname:     \"yaml flag sets output to yaml\",\n\t\t\tflags:    OutputFlags{YAML: true},\n\t\t\texpected: \"yaml\",\n\t\t},\n\t\t{\n\t\t\tname:     \"text flag sets output to text\",\n\t\t\tflags:    OutputFlags{Text: true},\n\t\t\texpected: \"text\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no flags leaves output empty\",\n\t\t\tflags:    OutputFlags{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"explicit output value is preserved when no bool flags set\",\n\t\t\tflags:    OutputFlags{Output: \"yaml\"},\n\t\t\texpected: \"yaml\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.flags.AfterApply()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\t\t\tif tt.flags.Output != tt.expected {\n\t\t\t\tt.Errorf(\"expected Output=%q, got %q\", tt.expected, tt.flags.Output)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/output/output.go",
    "content": "package output\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n// Format represents the output format type\ntype Format string\n\nconst (\n\t// FormatJSON outputs in JSON format\n\tFormatJSON Format = \"json\"\n\t// FormatYAML outputs in YAML format\n\tFormatYAML Format = \"yaml\"\n\t// FormatText outputs in plain text/default format\n\tFormatText    Format = \"text\"\n\tDefaultFormat Format = FormatJSON\n)\n\n// ResolveFormat determines the output format to use.\n// Priority: flagValue (if set) > configValue > DefaultFormat\nfunc ResolveFormat(flagValue, configValue string) Format {\n\tif flagValue != \"\" {\n\t\treturn Format(flagValue)\n\t}\n\tif configValue != \"\" {\n\t\treturn Format(configValue)\n\t}\n\treturn DefaultFormat\n}\n\n// Formatter is an interface that types must implement to support formatted output\ntype Formatter interface {\n\t// TextOutput returns the plain text representation\n\tTextOutput() string\n}\n\n// Write outputs the given value in the specified format to the writer\nfunc Write(w io.Writer, v interface{}, format Format) error {\n\tswitch format {\n\tcase FormatJSON:\n\t\treturn writeJSON(w, v)\n\tcase FormatYAML:\n\t\treturn writeYAML(w, v)\n\tcase FormatText:\n\t\treturn writeText(w, v)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported output format: %s\", format)\n\t}\n}\n\n// WriteTextOrStructured writes a human-readable text line for text output and\n// structured output for JSON or YAML formats.\nfunc WriteTextOrStructured(w io.Writer, format Format, structuredValue interface{}, text string) error {\n\tif format == FormatText {\n\t\t_, err := fmt.Fprintln(w, text)\n\t\treturn err\n\t}\n\n\treturn Write(w, structuredValue, format)\n}\n\nfunc writeJSON(w io.Writer, v interface{}) error {\n\tencoder := json.NewEncoder(w)\n\tencoder.SetIndent(\"\", \"  \")\n\treturn encoder.Encode(v)\n}\n\nfunc writeYAML(w io.Writer, v interface{}) error {\n\tencoder := yaml.NewEncoder(w)\n\tencoder.SetIndent(2)\n\treturn encoder.Encode(v)\n}\n\nfunc writeText(w io.Writer, v interface{}) error {\n\tif f, ok := v.(Formatter); ok {\n\t\t_, err := fmt.Fprintln(w, f.TextOutput())\n\t\treturn err\n\t}\n\t// Fallback to default string representation\n\t_, err := fmt.Fprintln(w, v)\n\treturn err\n}\n"
  },
  {
    "path": "pkg/output/output_test.go",
    "content": "package output\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestWriteTextOrStructured(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"writes text for text output\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar buf bytes.Buffer\n\t\tif err := WriteTextOrStructured(&buf, FormatText, []string{}, \"No pipelines found.\"); err != nil {\n\t\t\tt.Fatalf(\"WriteTextOrStructured() error = %v\", err)\n\t\t}\n\n\t\tif got := strings.TrimSpace(buf.String()); got != \"No pipelines found.\" {\n\t\t\tt.Fatalf(\"WriteTextOrStructured() = %q, want %q\", got, \"No pipelines found.\")\n\t\t}\n\t})\n\n\tt.Run(\"writes structured empty collections for json output\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar buf bytes.Buffer\n\t\tif err := WriteTextOrStructured(&buf, FormatJSON, []string{}, \"ignored\"); err != nil {\n\t\t\tt.Fatalf(\"WriteTextOrStructured() error = %v\", err)\n\t\t}\n\n\t\tif got := strings.TrimSpace(buf.String()); got != \"[]\" {\n\t\t\tt.Fatalf(\"WriteTextOrStructured() = %q, want %q\", got, \"[]\")\n\t\t}\n\t})\n\n\tt.Run(\"writes structured null values for json output\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar buf bytes.Buffer\n\t\tif err := WriteTextOrStructured(&buf, FormatJSON, nil, \"ignored\"); err != nil {\n\t\t\tt.Fatalf(\"WriteTextOrStructured() error = %v\", err)\n\t\t}\n\n\t\tif got := strings.TrimSpace(buf.String()); got != \"null\" {\n\t\t\tt.Fatalf(\"WriteTextOrStructured() = %q, want %q\", got, \"null\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "pkg/output/table.go",
    "content": "package output\n\nimport (\n\t\"math\"\n\t\"os\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/mattn/go-runewidth\"\n\t\"golang.org/x/term\"\n)\n\nconst (\n\tansiReset         = \"\\033[0m\"\n\tansiBold          = \"\\033[1m\"\n\tansiDim           = \"\\033[2m\"\n\tansiItalic        = \"\\033[3m\"\n\tansiUnderline     = \"\\033[4m\"\n\tansiDimUnder      = \"\\033[2;4m\"\n\tansiStrikeThrough = \"\\033[9m\"\n\tcolSeparator      = \"    \"\n\tminColumnWidth    = 3\n\tellipsisWidth     = 3\n\tdefaultTableWidth = 120\n)\n\n// ansiPattern strips ANSI/OSC escape sequences\nvar ansiPattern = regexp.MustCompile(`\\x1b(?:\\[[0-9;?]*[ -/]*[@-~]|\\][^\\a]*(?:\\a|\\x1b\\\\)|[P_\\]^][^\\x1b]*\\x1b\\\\)`)\n\nfunc Table(headers []string, rows [][]string, columnStyles map[string]string) string {\n\tif len(headers) == 0 {\n\t\treturn \"\"\n\t}\n\n\tuseColor := ColorEnabled()\n\tmaxWidth := detectedTableWidth()\n\n\tupperHeaders := make([]string, len(headers))\n\tcolStyles := make([]string, len(headers))\n\tfor i, header := range headers {\n\t\tupperHeaders[i] = strings.ToUpper(header)\n\n\t\tstyle := columnStyles[strings.ToLower(header)]\n\t\tif style != \"\" && useColor {\n\t\t\tswitch style {\n\t\t\tcase \"bold\":\n\t\t\t\tcolStyles[i] = ansiBold\n\t\t\tcase \"dim\":\n\t\t\t\tcolStyles[i] = ansiDim\n\t\t\tcase \"italic\":\n\t\t\t\tcolStyles[i] = ansiItalic\n\t\t\tcase \"underline\":\n\t\t\t\tcolStyles[i] = ansiUnderline\n\t\t\tcase \"strikethrough\":\n\t\t\t\tcolStyles[i] = ansiStrikeThrough\n\t\t\tdefault:\n\t\t\t\tcolStyles[i] = \"\"\n\t\t\t}\n\t\t}\n\t}\n\n\tcolWidths := make([]int, len(headers))\n\tfor i, header := range upperHeaders {\n\t\tcolWidths[i] = displayWidth(header)\n\t}\n\n\tfor _, row := range rows {\n\t\tfor i := 0; i < len(row) && i < len(colWidths); i++ {\n\t\t\tif width := displayWidth(row[i]); width > colWidths[i] {\n\t\t\t\tcolWidths[i] = width\n\t\t\t}\n\t\t}\n\t}\n\n\tcolWidths = clampColumnWidths(colWidths, len(headers), len(colSeparator), maxWidth)\n\n\ttotalWidth := 0\n\tfor _, width := range colWidths {\n\t\ttotalWidth += width + len(colSeparator)\n\t}\n\tconst maxEstimatedSize = 1 << 20\n\testimatedSize := totalWidth * (len(rows) + 1)\n\tif estimatedSize < 0 || estimatedSize > maxEstimatedSize {\n\t\testimatedSize = maxEstimatedSize\n\t}\n\tvar builder strings.Builder\n\tbuilder.Grow(estimatedSize)\n\n\tfor i, upperHeader := range upperHeaders {\n\t\tif useColor {\n\t\t\tbuilder.WriteString(ansiDimUnder)\n\t\t}\n\t\twritePadded(&builder, truncateToWidth(upperHeader, colWidths[i]), colWidths[i])\n\t\tif useColor {\n\t\t\tbuilder.WriteString(ansiReset)\n\t\t}\n\t\tif i < len(headers)-1 && colWidths[i] > 0 {\n\t\t\tbuilder.WriteString(colSeparator)\n\t\t}\n\t}\n\tbuilder.WriteString(\"\\n\")\n\n\tfor _, row := range rows {\n\t\tfor i := range headers {\n\t\t\tvalue := \"\"\n\t\t\tif i < len(row) {\n\t\t\t\tvalue = row[i]\n\t\t\t}\n\n\t\t\tvalue = truncateToWidth(value, colWidths[i])\n\n\t\t\tif colStyles[i] != \"\" {\n\t\t\t\tbuilder.WriteString(colStyles[i])\n\t\t\t}\n\n\t\t\twritePadded(&builder, value, colWidths[i])\n\n\t\t\tif colStyles[i] != \"\" {\n\t\t\t\tbuilder.WriteString(ansiReset)\n\t\t\t}\n\n\t\t\tif i < len(headers)-1 && colWidths[i] > 0 {\n\t\t\t\tbuilder.WriteString(colSeparator)\n\t\t\t}\n\t\t}\n\t\tbuilder.WriteString(\"\\n\")\n\t}\n\n\treturn builder.String()\n}\n\nfunc displayWidth(s string) int {\n\tstripped := ansiPattern.ReplaceAllString(s, \"\")\n\treturn runewidth.StringWidth(stripped)\n}\n\nfunc writePadded(builder *strings.Builder, s string, width int) {\n\tvisible := displayWidth(s)\n\tbuilder.WriteString(s)\n\tfor i := visible; i < width; i++ {\n\t\tbuilder.WriteByte(' ')\n\t}\n}\n\nfunc truncateToWidth(s string, width int) string {\n\tif width <= 0 {\n\t\treturn \"\"\n\t}\n\n\tif displayWidth(s) <= width {\n\t\treturn s\n\t}\n\n\tif width <= ellipsisWidth {\n\t\treturn trimToWidth(s, width)\n\t}\n\n\ttrimmed := trimToWidth(s, width-ellipsisWidth)\n\treturn trimmed + \"...\"\n}\n\nfunc trimToWidth(s string, width int) string {\n\tif width <= 0 {\n\t\treturn \"\"\n\t}\n\n\tstripped := ansiPattern.ReplaceAllString(s, \"\")\n\tif runewidth.StringWidth(stripped) <= width {\n\t\treturn s\n\t}\n\n\tvar b strings.Builder\n\tb.Grow(len(s))\n\n\tcurrentWidth := 0\n\ti := 0\n\n\tfor i < len(s) {\n\t\tif s[i] == '\\x1b' {\n\t\t\tif loc := ansiPattern.FindStringIndex(s[i:]); loc != nil && loc[0] == 0 {\n\t\t\t\tb.WriteString(s[i : i+loc[1]])\n\t\t\t\ti += loc[1]\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tr, size := utf8.DecodeRuneInString(s[i:])\n\t\tif r == utf8.RuneError {\n\t\t\tbreak\n\t\t}\n\n\t\trw := runewidth.RuneWidth(r)\n\t\tif rw == 0 {\n\t\t\tb.WriteString(s[i : i+size])\n\t\t\ti += size\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentWidth+rw > width {\n\t\t\tbreak\n\t\t}\n\n\t\tb.WriteString(s[i : i+size])\n\t\tcurrentWidth += rw\n\t\ti += size\n\t}\n\n\treturn b.String()\n}\n\nfunc clampColumnWidths(colWidths []int, colCount, separatorWidth, maxWidth int) []int {\n\tif maxWidth <= 0 || colCount == 0 {\n\t\treturn colWidths\n\t}\n\n\tsepTotal := (colCount - 1) * separatorWidth\n\tif sepTotal >= maxWidth {\n\t\tclamped := make([]int, len(colWidths))\n\t\treturn clamped\n\t}\n\n\tavailable := maxWidth - sepTotal\n\tsum := 0\n\tfor _, width := range colWidths {\n\t\tsum += width\n\t}\n\n\tif sum <= available {\n\t\treturn colWidths\n\t}\n\n\tclamped := make([]int, len(colWidths))\n\tif sum == 0 {\n\t\tfor i := range clamped {\n\t\t\tclamped[i] = minColumnWidth\n\t\t}\n\t\treturn clamped\n\t}\n\n\teffectiveMin := minColumnWidth\n\tif available < minColumnWidth*colCount {\n\t\teffectiveMin = available / colCount\n\t}\n\t// First pass: give narrow columns their full width, mark wide columns for proportional allocation\n\t// A column is \"narrow\" if it fits within its fair share of space\n\tfairShare := available / colCount\n\tnarrowThreshold := fairShare * 2\n\n\tfixed := make([]bool, len(colWidths))\n\tremainingSpace := available\n\tflexSum := 0\n\n\tfor i, width := range colWidths {\n\t\tif width <= narrowThreshold {\n\t\t\t// This column is narrow enough to get its full width\n\t\t\tclamped[i] = width\n\t\t\tfixed[i] = true\n\t\t\tremainingSpace -= width\n\t\t} else {\n\t\t\t// This column needs proportional allocation\n\t\t\tflexSum += width\n\t\t}\n\t}\n\n\t// Second pass: proportionally allocate remaining space to flexible columns\n\tfor i, width := range colWidths {\n\t\tif fixed[i] {\n\t\t\tcontinue\n\t\t}\n\t\tif flexSum == 0 {\n\t\t\tclamped[i] = effectiveMin\n\t\t\tcontinue\n\t\t}\n\t\tratio := float64(width) / float64(flexSum)\n\t\talloc := int(math.Floor(ratio * float64(remainingSpace)))\n\t\tclamped[i] = max(alloc, effectiveMin)\n\t}\n\n\tcurrentTotal := 0\n\tfor _, width := range clamped {\n\t\tcurrentTotal += width\n\t}\n\n\tremaining := available - currentTotal\n\n\ttype columnIndex struct {\n\t\toriginalWidth int\n\t\tindex         int\n\t}\n\n\tindices := make([]columnIndex, len(colWidths))\n\tfor i, width := range colWidths {\n\t\tindices[i] = columnIndex{originalWidth: width, index: i}\n\t}\n\n\tsort.Slice(indices, func(i, j int) bool {\n\t\treturn indices[i].originalWidth > indices[j].originalWidth\n\t})\n\n\tif remaining > 0 {\n\t\tfor remaining > 0 {\n\t\t\tfor _, col := range indices {\n\t\t\t\tif remaining == 0 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tclamped[col.index]++\n\t\t\t\tremaining--\n\t\t\t}\n\t\t}\n\t} else if remaining < 0 {\n\t\tfor _, col := range indices {\n\t\t\tif remaining == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treduction := -remaining\n\t\t\tif reduction > clamped[col.index] {\n\t\t\t\treduction = clamped[col.index]\n\t\t\t}\n\t\t\tclamped[col.index] -= reduction\n\t\t\tremaining += reduction\n\t\t}\n\t}\n\n\treturn clamped\n}\n\nfunc detectedTableWidth() int {\n\tif override := os.Getenv(\"BUILDKITE_TABLE_MAX_WIDTH\"); override != \"\" {\n\t\tif parsed, err := strconv.Atoi(strings.TrimSpace(override)); err == nil && parsed > 0 {\n\t\t\treturn parsed\n\t\t}\n\t}\n\n\tfd := os.Stdout.Fd()\n\tif !isatty.IsTerminal(fd) && !isatty.IsCygwinTerminal(fd) {\n\t\treturn defaultTableWidth\n\t}\n\n\twidth, _, err := term.GetSize(int(fd))\n\tif err != nil || width <= 0 {\n\t\treturn defaultTableWidth\n\t}\n\n\treturn width\n}\n"
  },
  {
    "path": "pkg/output/table_test.go",
    "content": "package output\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"unicode/utf8\"\n)\n\nfunc TestTableTruncatesWhenWidthExceeded(t *testing.T) {\n\tt.Setenv(\"BUILDKITE_TABLE_MAX_WIDTH\", \"20\")\n\n\theaders := []string{\"Col1\", \"Col2\"}\n\trows := [][]string{{\"this-is-a-very-long-value\", \"short\"}}\n\n\ttable := Table(headers, rows, nil)\n\n\tlines := strings.Split(strings.TrimSuffix(table, \"\\n\"), \"\\n\")\n\tfor i, line := range lines {\n\t\tif displayWidth(line) > 20 {\n\t\t\tt.Fatalf(\"line %d exceeds max width: %d > 20\", i, displayWidth(line))\n\t\t}\n\t}\n\n\tif !strings.Contains(table, \"...\") {\n\t\tt.Fatalf(\"expected truncated output to contain ellipsis\")\n\t}\n}\n\nfunc TestTableProportionalClampPreservesShortColumn(t *testing.T) {\n\tt.Setenv(\"BUILDKITE_TABLE_MAX_WIDTH\", \"30\")\n\n\theaders := []string{\"Short\", \"Longer\"}\n\trows := [][]string{{\"ok\", \"this-is-a-very-long-value-that-should-truncate\"}}\n\n\ttable := Table(headers, rows, nil)\n\n\tlines := strings.Split(strings.TrimSuffix(table, \"\\n\"), \"\\n\")\n\tfor i, line := range lines {\n\t\tif displayWidth(line) > 30 {\n\t\t\tt.Fatalf(\"line %d exceeds max width: %d > 30\", i, displayWidth(line))\n\t\t}\n\t}\n\n\tif strings.Contains(lines[1], \"ok...\") {\n\t\tt.Fatalf(\"short column should not be truncated: %q\", lines[1])\n\t}\n\n\tif !strings.Contains(lines[1], \"...\") {\n\t\tt.Fatalf(\"long column should be truncated with ellipsis\")\n\t}\n}\n\nfunc TestTableRespectsNoTruncationWhenWidthIsLarge(t *testing.T) {\n\tt.Setenv(\"BUILDKITE_TABLE_MAX_WIDTH\", \"200\")\n\n\theaders := []string{\"Col1\", \"Col2\"}\n\trows := [][]string{{\"alpha\", \"beta\"}}\n\n\ttable := Table(headers, rows, nil)\n\n\tif !strings.Contains(table, \"alpha\") || !strings.Contains(table, \"beta\") {\n\t\tt.Fatalf(\"expected table to contain original values\")\n\t}\n\n\tfor _, line := range strings.Split(strings.TrimSuffix(table, \"\\n\"), \"\\n\") {\n\t\tif displayWidth(line) > 200 {\n\t\t\tt.Fatalf(\"line exceeds large width guard\")\n\t\t}\n\t}\n}\n\nfunc TestTableFitsWhenMaxWidthSmallerThanColumnCount(t *testing.T) {\n\tt.Setenv(\"BUILDKITE_TABLE_MAX_WIDTH\", \"20\")\n\n\theaders := []string{\"A\", \"B\", \"C\", \"D\"}\n\trows := [][]string{{\"val1\", \"val2\", \"val3\", \"val4\"}}\n\n\ttable := Table(headers, rows, nil)\n\n\tlines := strings.Split(strings.TrimSuffix(table, \"\\n\"), \"\\n\")\n\tfor i, line := range lines {\n\t\twidth := displayWidth(line)\n\t\tif width > 20 {\n\t\t\tt.Fatalf(\"line %d exceeds max width: %d > 20 (content: %q)\", i, width, line)\n\t\t}\n\t}\n\n\tif len(table) == 0 {\n\t\tt.Fatalf(\"table should not be empty even with severe constraints\")\n\t}\n}\n\nfunc TestTableFitsWhenSeparatorsExceedMaxWidth(t *testing.T) {\n\tt.Setenv(\"BUILDKITE_TABLE_MAX_WIDTH\", \"5\")\n\n\theaders := []string{\"A\", \"B\", \"C\"}\n\trows := [][]string{{\"x\", \"y\", \"z\"}}\n\n\ttable := Table(headers, rows, nil)\n\n\tlines := strings.Split(strings.TrimSuffix(table, \"\\n\"), \"\\n\")\n\tfor i, line := range lines {\n\t\twidth := displayWidth(line)\n\t\tif width > 5 {\n\t\t\tt.Fatalf(\"line %d exceeds max width: %d > 5 (content: %q)\", i, width, line)\n\t\t}\n\t}\n\n\tif len(table) == 0 {\n\t\tt.Fatalf(\"table should render even when separators exceed width\")\n\t}\n}\n\nfunc TestTrimToWidthPreservesANSICodes(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\twidth       int\n\t\tshouldMatch func(string) bool\n\t}{\n\t\t{\n\t\t\tname:  \"ANSI code at start\",\n\t\t\tinput: \"\\x1b[31mHello World\\x1b[0m\",\n\t\t\twidth: 5,\n\t\t\tshouldMatch: func(result string) bool {\n\t\t\t\treturn strings.HasPrefix(result, \"\\x1b[31m\") && strings.Contains(result, \"Hello\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"ANSI code in middle - preserves codes before truncation\",\n\t\t\tinput: \"Hello \\x1b[31mWorld\\x1b[0m\",\n\t\t\twidth: 8,\n\t\t\tshouldMatch: func(result string) bool {\n\t\t\t\t// Should contain \"Hello \" and the color code, with \"Wo\"\n\t\t\t\treturn strings.Contains(result, \"Hello\") && strings.Contains(result, \"\\x1b[31m\") && strings.Contains(result, \"Wo\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"Multiple ANSI codes\",\n\t\t\tinput: \"\\x1b[1m\\x1b[31mBold Red\\x1b[0m\",\n\t\t\twidth: 4,\n\t\t\tshouldMatch: func(result string) bool {\n\t\t\t\treturn strings.Contains(result, \"\\x1b[1m\") && strings.Contains(result, \"\\x1b[31m\") && strings.Contains(result, \"Bold\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"ANSI code after truncation point - not included\",\n\t\t\tinput: \"Hello\\x1b[31m World\\x1b[0m\",\n\t\t\twidth: 5,\n\t\t\tshouldMatch: func(result string) bool {\n\t\t\t\treturn result == \"Hello\" || result == \"Hello\\x1b[31m\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"Wide characters with ANSI\",\n\t\t\tinput: \"\\x1b[32m你好世界\\x1b[0m\",\n\t\t\twidth: 4,\n\t\t\tshouldMatch: func(result string) bool {\n\t\t\t\treturn strings.HasPrefix(result, \"\\x1b[32m\") && strings.Contains(result, \"你好\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := trimToWidth(tt.input, tt.width)\n\n\t\t\tif !tt.shouldMatch(result) {\n\t\t\t\tt.Errorf(\"trimToWidth(%q, %d) = %q failed validation\", tt.input, tt.width, result)\n\t\t\t}\n\n\t\t\tif strings.Contains(result, \"\\x1b\") {\n\t\t\t\tmatches := ansiPattern.FindAllString(result, -1)\n\n\t\t\t\tif strings.Count(result, \"\\x1b\") != len(matches) {\n\t\t\t\t\tt.Errorf(\"Result contains broken ANSI sequences: %q (found %d escape chars but %d complete sequences)\",\n\t\t\t\t\t\tresult, strings.Count(result, \"\\x1b\"), len(matches))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tactualWidth := displayWidth(result)\n\t\t\tif actualWidth > tt.width {\n\t\t\t\tt.Errorf(\"Result width %d exceeds requested width %d\", actualWidth, tt.width)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTableHandlesComplexUnicode(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\theaders []string\n\t\trows    [][]string\n\t\tverify  func(*testing.T, string)\n\t}{\n\t\t{\n\t\t\tname:    \"Emoji with zero-width joiners\",\n\t\t\theaders: []string{\"Family\", \"Description\"},\n\t\t\trows: [][]string{\n\t\t\t\t{\"👨‍👩‍👧‍👦\", \"Family with kids\"},\n\t\t\t\t{\"👩‍💻\", \"Woman technologist\"},\n\t\t\t},\n\t\t\tverify: func(t *testing.T, result string) {\n\t\t\t\tif !strings.Contains(result, \"👨‍👩‍👧‍👦\") {\n\t\t\t\t\tt.Error(\"Family emoji with ZWJ missing from output\")\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(result, \"👩‍💻\") {\n\t\t\t\t\tt.Error(\"Woman technologist emoji missing from output\")\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Right-to-left text (Hebrew)\",\n\t\t\theaders: []string{\"Hebrew\", \"English\"},\n\t\t\trows: [][]string{\n\t\t\t\t{\"שלום\", \"Hello\"},\n\t\t\t\t{\"עברית\", \"Hebrew\"},\n\t\t\t},\n\t\t\tverify: func(t *testing.T, result string) {\n\t\t\t\tif !strings.Contains(result, \"שלום\") {\n\t\t\t\t\tt.Error(\"Hebrew text missing from output\")\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(result, \"עברית\") {\n\t\t\t\t\tt.Error(\"Hebrew word for 'Hebrew' missing from output\")\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Right-to-left text (Arabic)\",\n\t\t\theaders: []string{\"Arabic\", \"English\"},\n\t\t\trows: [][]string{\n\t\t\t\t{\"مرحبا\", \"Hello\"},\n\t\t\t\t{\"العربية\", \"Arabic\"},\n\t\t\t},\n\t\t\tverify: func(t *testing.T, result string) {\n\t\t\t\tif !strings.Contains(result, \"مرحبا\") {\n\t\t\t\t\tt.Error(\"Arabic text missing from output\")\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(result, \"العربية\") {\n\t\t\t\t\tt.Error(\"Arabic word for 'Arabic' missing from output\")\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Combining diacritical marks\",\n\t\t\theaders: []string{\"Text\", \"Type\"},\n\t\t\trows: [][]string{\n\t\t\t\t{\"café\", \"Precomposed\"},\n\t\t\t\t{\"café\", \"Combining marks\"},\n\t\t\t\t{\"ñ vs ñ\", \"Different forms\"},\n\t\t\t},\n\t\t\tverify: func(t *testing.T, result string) {\n\t\t\t\t// Both forms of café should be present\n\t\t\t\tcafeCount := strings.Count(result, \"café\")\n\t\t\t\tif cafeCount < 1 {\n\t\t\t\t\tt.Errorf(\"Expected at least one 'café', got %d occurrences\", cafeCount)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Mixed emoji and text\",\n\t\t\theaders: []string{\"Status\", \"Message\"},\n\t\t\trows: [][]string{\n\t\t\t\t{\"✅\", \"Success\"},\n\t\t\t\t{\"❌\", \"Failed\"},\n\t\t\t\t{\"⚠️\", \"Warning\"},\n\t\t\t},\n\t\t\tverify: func(t *testing.T, result string) {\n\t\t\t\tif !strings.Contains(result, \"✅\") {\n\t\t\t\t\tt.Error(\"Check mark emoji missing\")\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(result, \"❌\") {\n\t\t\t\t\tt.Error(\"Cross mark emoji missing\")\n\t\t\t\t}\n\t\t\t\tif !strings.Contains(result, \"⚠\") {\n\t\t\t\t\tt.Error(\"Warning emoji missing\")\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Skin tone modifiers\",\n\t\t\theaders: []string{\"Emoji\", \"Description\"},\n\t\t\trows: [][]string{\n\t\t\t\t{\"👋\", \"Wave (default)\"},\n\t\t\t\t{\"👋🏻\", \"Wave (light skin)\"},\n\t\t\t\t{\"👋🏿\", \"Wave (dark skin)\"},\n\t\t\t},\n\t\t\tverify: func(t *testing.T, result string) {\n\t\t\t\tif !strings.Contains(result, \"👋\") {\n\t\t\t\t\tt.Error(\"Wave emoji missing\")\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Regional indicator symbols (flags)\",\n\t\t\theaders: []string{\"Flag\", \"Country\"},\n\t\t\trows: [][]string{\n\t\t\t\t{\"🇺🇸\", \"United States\"},\n\t\t\t\t{\"🇬🇧\", \"United Kingdom\"},\n\t\t\t\t{\"🇯🇵\", \"Japan\"},\n\t\t\t},\n\t\t\tverify: func(t *testing.T, result string) {\n\t\t\t\t// Flags are made of regional indicator pairs\n\t\t\t\tif !strings.Contains(result, \"🇺🇸\") {\n\t\t\t\t\tt.Error(\"US flag emoji missing\")\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Variation selectors\",\n\t\t\theaders: []string{\"Char\", \"Type\"},\n\t\t\trows: [][]string{\n\t\t\t\t{\"♠\", \"Text style\"},\n\t\t\t\t{\"♠️\", \"Emoji style\"},\n\t\t\t},\n\t\t\tverify: func(t *testing.T, result string) {\n\t\t\t\tif !strings.Contains(result, \"♠\") {\n\t\t\t\t\tt.Error(\"Spade symbol missing\")\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Table(tt.headers, tt.rows, nil)\n\n\t\t\t// Verify the table was generated\n\t\t\tif len(result) == 0 {\n\t\t\t\tt.Fatal(\"Table output is empty\")\n\t\t\t}\n\n\t\t\t// Verify headers are present\n\t\t\tfor _, header := range tt.headers {\n\t\t\t\tupperHeader := strings.ToUpper(header)\n\t\t\t\tif !strings.Contains(result, upperHeader) {\n\t\t\t\t\tt.Errorf(\"Header %q not found in output\", upperHeader)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Run custom verification\n\t\t\ttt.verify(t, result)\n\n\t\t\t// Verify the output doesn't have broken formatting\n\t\t\tlines := strings.Split(strings.TrimSuffix(result, \"\\n\"), \"\\n\")\n\t\t\tif len(lines) != len(tt.rows)+1 {\n\t\t\t\tt.Errorf(\"Expected %d lines (1 header + %d rows), got %d\", len(tt.rows)+1, len(tt.rows), len(lines))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTrimToWidthWithComplexUnicode(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\twidth    int\n\t\tminWidth int // minimum acceptable width of result\n\t}{\n\t\t{\n\t\t\tname:     \"Emoji with ZWJ truncation\",\n\t\t\tinput:    \"Hello 👨‍👩‍👧‍👦 World\",\n\t\t\twidth:    10,\n\t\t\tminWidth: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"Arabic text truncation\",\n\t\t\tinput:    \"مرحبا بك في العالم\",\n\t\t\twidth:    8,\n\t\t\tminWidth: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"Hebrew text truncation\",\n\t\t\tinput:    \"שלום עולם מקסים\",\n\t\t\twidth:    6,\n\t\t\tminWidth: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"Combined diacritics truncation\",\n\t\t\tinput:    \"Café résumé naïve\",\n\t\t\twidth:    8,\n\t\t\tminWidth: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"Flag emoji truncation\",\n\t\t\tinput:    \"USA: 🇺🇸 UK: 🇬🇧 JP: 🇯🇵\",\n\t\t\twidth:    12,\n\t\t\tminWidth: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"Skin tone modifiers truncation\",\n\t\t\tinput:    \"👋 👋🏻 👋🏿\",\n\t\t\twidth:    6,\n\t\t\tminWidth: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := trimToWidth(tt.input, tt.width)\n\n\t\t\tactualWidth := displayWidth(result)\n\t\t\tif actualWidth > tt.width {\n\t\t\t\tt.Errorf(\"Result width %d exceeds max width %d (input: %q, result: %q)\",\n\t\t\t\t\tactualWidth, tt.width, tt.input, result)\n\t\t\t}\n\n\t\t\tif actualWidth < tt.minWidth {\n\t\t\t\tt.Errorf(\"Result width %d is less than min width %d (result: %q)\",\n\t\t\t\t\tactualWidth, tt.minWidth, result)\n\t\t\t}\n\n\t\t\t// Verify we didn't create invalid UTF-8\n\t\t\tif !utf8.ValidString(result) {\n\t\t\t\tt.Errorf(\"Result contains invalid UTF-8: %q\", result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/output/value.go",
    "content": "package output\n\nimport \"strings\"\n\nfunc ValueOrDash(s string) string {\n\tif strings.TrimSpace(s) == \"\" {\n\t\treturn \"-\"\n\t}\n\treturn s\n}\n"
  },
  {
    "path": "pkg/output/viewable.go",
    "content": "package output\n\nimport (\n\t\"encoding/json\"\n)\n\n// Viewable wraps any type to provide formatted output support.\n// It delegates JSON/YAML marshaling directly to the underlying data,\n// while using a custom render function for text output.\ntype Viewable[T any] struct {\n\tData   T\n\tRender func(T) string\n}\n\n// TextOutput implements the Formatter interface for text output.\nfunc (v Viewable[T]) TextOutput() string {\n\treturn v.Render(v.Data)\n}\n\n// MarshalJSON delegates JSON marshaling to the underlying data.\nfunc (v Viewable[T]) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(v.Data)\n}\n\n// MarshalYAML delegates YAML marshaling to the underlying data.\nfunc (v Viewable[T]) MarshalYAML() (interface{}, error) {\n\treturn v.Data, nil\n}\n"
  },
  {
    "path": "pkg/output/viewable_test.go",
    "content": "package output\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestViewable_TextOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttype Data struct {\n\t\tName  string `json:\"name\"`\n\t\tValue int    `json:\"value\"`\n\t}\n\n\tv := Viewable[Data]{\n\t\tData: Data{Name: \"test\", Value: 42},\n\t\tRender: func(d Data) string {\n\t\t\treturn \"Name: \" + d.Name\n\t\t},\n\t}\n\n\tgot := v.TextOutput()\n\twant := \"Name: test\"\n\tif got != want {\n\t\tt.Errorf(\"TextOutput() = %q, want %q\", got, want)\n\t}\n}\n\nfunc TestViewable_MarshalJSON(t *testing.T) {\n\tt.Parallel()\n\n\ttype Data struct {\n\t\tName  string `json:\"name\"`\n\t\tValue int    `json:\"value\"`\n\t}\n\n\tv := Viewable[Data]{\n\t\tData:   Data{Name: \"test\", Value: 42},\n\t\tRender: func(d Data) string { return \"\" },\n\t}\n\n\tgot, err := json.Marshal(v)\n\tif err != nil {\n\t\tt.Fatalf(\"MarshalJSON() error = %v\", err)\n\t}\n\n\tvar unmarshaled Data\n\tif err := json.Unmarshal(got, &unmarshaled); err != nil {\n\t\tt.Fatalf(\"Unmarshal() error = %v\", err)\n\t}\n\n\tif unmarshaled.Name != \"test\" || unmarshaled.Value != 42 {\n\t\tt.Errorf(\"MarshalJSON() produced incorrect data: %+v\", unmarshaled)\n\t}\n}\n\nfunc TestViewable_MarshalYAML(t *testing.T) {\n\tt.Parallel()\n\n\ttype Data struct {\n\t\tName  string `yaml:\"name\"`\n\t\tValue int    `yaml:\"value\"`\n\t}\n\n\tv := Viewable[Data]{\n\t\tData:   Data{Name: \"test\", Value: 42},\n\t\tRender: func(d Data) string { return \"\" },\n\t}\n\n\tgot, err := yaml.Marshal(v)\n\tif err != nil {\n\t\tt.Fatalf(\"MarshalYAML() error = %v\", err)\n\t}\n\n\tvar unmarshaled Data\n\tif err := yaml.Unmarshal(got, &unmarshaled); err != nil {\n\t\tt.Fatalf(\"Unmarshal() error = %v\", err)\n\t}\n\n\tif unmarshaled.Name != \"test\" || unmarshaled.Value != 42 {\n\t\tt.Errorf(\"MarshalYAML() produced incorrect data: %+v\", unmarshaled)\n\t}\n}\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"config:recommended\",\n    \":disableDependencyDashboard\",\n    \":semanticCommits\",\n    \":automergeMinor\",\n    \"schedule:nonOfficeHours\"\n  ],\n  \"labels\": [\"dependencies\"],\n  \"postUpdateOptions\": [\"gomodTidy\"],\n  \"packageRules\": [\n    {\n      \"description\": \"Group Buildkite dependencies\",\n      \"matchSourceUrls\": [\"https://github.com/buildkite/**\"],\n      \"groupName\": \"buildkite\",\n      \"automerge\": true\n    },\n    {\n      \"description\": \"Group Buildkite pipeline config\",\n      \"matchFileNames\": [\".buildkite/**\"],\n      \"groupName\": \"buildkite\"\n    },\n    {\n      \"description\": \"Group Go minor and patch updates\",\n      \"matchManagers\": [\"gomod\"],\n      \"matchUpdateTypes\": [\"minor\", \"patch\"],\n      \"groupName\": \"go-minor-patch\",\n      \"automerge\": true\n    },\n    {\n      \"description\": \"Group golang.org/x/ packages\",\n      \"matchSourceUrls\": [\"https://github.com/golang/**\"],\n      \"groupName\": \"golang-x\",\n      \"automerge\": true\n    },\n    {\n      \"description\": \"Group test and dev tooling\",\n      \"matchPackageNames\": [\n        \"github.com/stretchr/testify\",\n        \"github.com/alecthomas/assert\"\n      ],\n      \"groupName\": \"testing\",\n      \"automerge\": true\n    },\n    {\n      \"description\": \"Do not automerge major updates\",\n      \"matchUpdateTypes\": [\"major\"],\n      \"automerge\": false\n    }\n  ],\n  \"automerge\": true,\n  \"automergeType\": \"pr\",\n  \"platformAutomerge\": true\n}\n"
  },
  {
    "path": "schema.graphql",
    "content": "\"\"\"Directs the executor to include this field or fragment only when the `if` argument is true.\"\"\"\ndirective @include(\n\"\"\"Included when true.\"\"\"\n\tif: Boolean!\n) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\n\"\"\"Directs the executor to skip this field or fragment when the `if` argument is true.\"\"\"\ndirective @skip(\n\"\"\"Skipped when true.\"\"\"\n\tif: Boolean!\n) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT\n\n\"\"\"Marks an element of a GraphQL schema as no longer supported.\"\"\"\ndirective @deprecated(\n\"\"\"Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).\"\"\"\n\treason: String\n) on FIELD_DEFINITION | ENUM_VALUE | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION\n\n\"\"\"Requires that exactly one field must be supplied and that field must not be `null`.\"\"\"\ndirective @oneOf on INPUT_OBJECT\n\n\"\"\"API access tokens for authentication with the Buildkite API\"\"\"\ntype APIAccessToken implements Node{\n\tid: ID!\n\"\"\"The public UUID for the API Access Token\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"A code that is used by an API Application to request an API Access Token\"\"\"\ntype APIAccessTokenCode implements Node{\n\tapplication: APIApplication\n\"\"\"The time when this code was authorized by a user\"\"\"\n\tauthorizedAt: DateTime\n\"\"\"The IP address of the client that authorized this code\"\"\"\n\tauthorizedIPAddress: String\n\"\"\"The actual code used to find this API Access Token Code record\"\"\"\n\tcode: String!\n\"\"\"The description of the code provided by the API Application\"\"\"\n\tdescription: String!\n\"\"\"The time when this code will expire\"\"\"\n\texpiresAt: DateTime!\n\tid: ID!\n}\n\n\"\"\"Autogenerated input type of APIAccessTokenCodeAuthorizeMutation\"\"\"\ninput APIAccessTokenCodeAuthorizeMutationInput {\n\"\"\"Autogenerated input type of APIAccessTokenCodeAuthorizeMutation\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of APIAccessTokenCodeAuthorizeMutation\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of APIAccessTokenCodeAuthorizeMutation.\"\"\"\ntype APIAccessTokenCodeAuthorizeMutationPayload {\n\tapiAccessTokenCode: APIAccessTokenCode!\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n}\n\n\"\"\"All possible scopes on a user's API Access Token\"\"\"\nenum APIAccessTokenScopes {\n\tGRAPHQL\n\tREAD_AGENTS\n\tREAD_ARTIFACTS\n\tREAD_BUILD_LOGS\n\tREAD_BUILDS\n\tREAD_CLUSTERS\n\tREAD_JOB_ENV\n\tREAD_NOTIFICATION_SERVICES\n\tREAD_ORGANIZATIONS\n\tREAD_PIPELINE_TEMPLATES\n\tREAD_PIPELINES\n\tREAD_SUITES\n\tREAD_TEAMS\n\tREAD_USER\n\tWRITE_AGENTS\n\tWRITE_ARTIFACTS\n\tWRITE_BUILD_LOGS\n\tWRITE_BUILDS\n\tWRITE_CLUSTERS\n\tWRITE_NOTIFICATION_SERVICES\n\tWRITE_PIPELINE_TEMPLATES\n\tWRITE_PIPELINES\n\tWRITE_SUITES\n\tWRITE_TEAMS\n}\n\n\"\"\"An API Application\"\"\"\ntype APIApplication implements Node{\n\"\"\"A description of the application\"\"\"\n\tdescription: String!\n\tid: ID!\n\"\"\"The name of this application\"\"\"\n\tname: String!\n}\n\n\"\"\"An agent\"\"\"\ntype Agent implements Node{\n\tclusterQueue: ClusterQueue\n\"\"\"The time when the agent connected to Buildkite\"\"\"\n\tconnectedAt: DateTime\n\"\"\"The connection state of the agent\"\"\"\n\tconnectionState: String!\n\"\"\"The date the agent was created\"\"\"\n\tcreatedAt: DateTime\n\"\"\"The time when the agent disconnected from Buildkite\"\"\"\n\tdisconnectedAt: DateTime\n\"\"\"The last time the agent performed a `heartbeat` operation to the Agent API\"\"\"\n\theartbeatAt: DateTime\n\"\"\"The hostname of the machine running the agent\"\"\"\n\thostname: String\n\tid: ID!\n\"\"\"The IP address that the agent has connected from\"\"\"\n\tipAddress: String\n\"\"\"If this version of agent has been deprecated by Buildkite\"\"\"\n\tisDeprecated: Boolean!\n\"\"\"Returns whether or not this agent is running a job. If isRunningJob true, but the `job` field is empty, the current user doesn't have access to view the job\"\"\"\n\tisRunningJob: Boolean!\n\"\"\"The currently running job\"\"\"\n\tjob: Job\n\"\"\"Jobs that have been assigned to this agent\"\"\"\n\tjobs(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t\ttype: [JobTypes!]\n\t\tstate: [JobStates!]\n\t\tpriority: Int\n\t\tagentQueryRules: [String!]\n\t\tconcurrency: JobConcurrencySearch\n\"\"\"Whether or not the command job passed. Passing `false` will return all failed jobs (including \"soft failed\" jobs)\"\"\"\n\t\tpassed: Boolean\n\"\"\"Filtering jobs based on related step information\"\"\"\n\t\tstep: JobStepSearch\n\"\"\"Order the jobs\"\"\"\n\t\torder: JobOrder\n\t): JobConnection\n\"\"\"The date the agent was lost from Buildkite if it didn't cleanly disconnect\"\"\"\n\tlostAt: DateTime\n\"\"\"The meta data this agent was stared with\"\"\"\n\tmetaData: [String!]\n\"\"\"The name of the agent\"\"\"\n\tname: String!\n\"\"\"The operating system the agent is running on\"\"\"\n\toperatingSystem: OperatingSystem\n\torganization: Organization\n\tpermissions: AgentPermissions!\n\"\"\"The process identifier (PID) of the agent process on the machine\"\"\"\n\tpid: String\n\tpingedAt: DateTime\n\"\"\"The priority setting for the agent\"\"\"\n\tpriority: Int\n\"\"\"Whether this agent is visible to everyone, including people outside this organization\"\"\"\n\tpublic: Boolean!\n\"\"\"The time this agent was forced to stop\"\"\"\n\tstopForcedAt: DateTime\n\"\"\"The user that forced this agent to stop\"\"\"\n\tstopForcedBy: User\n\"\"\"The time the agent was first asked to stop\"\"\"\n\tstoppedAt: DateTime\n\"\"\"The user that initially stopped this agent\"\"\"\n\tstoppedBy: User\n\"\"\"The time the agent was gracefully stopped by a user\"\"\"\n\tstoppedGracefullyAt: DateTime\n\"\"\"The user that gracefully stopped this agent\"\"\"\n\tstoppedGracefullyBy: User\n\"\"\"The User-Agent of the program that is making Agent API requests to Buildkite\"\"\"\n\tuserAgent: String\n\"\"\"The public UUID for the agent\"\"\"\n\tuuid: String!\n\"\"\"The version of the agent\"\"\"\n\tversion: String\n\"\"\"Whether this agent's version has known issues and should be upgraded\"\"\"\n\tversionHasKnownIssues: Boolean!\n}\n\ntype AgentConnection implements Connection{\n\tcount: Int!\n\tedges: [AgentEdge]\n\tpageInfo: PageInfo\n}\n\ntype AgentEdge {\n\tcursor: String!\n\tnode: Agent\n}\n\n\"\"\"Permissions information about what actions the current user can do against this agent\"\"\"\ntype AgentPermissions {\n\"\"\"Whether the user can stop the agent remotely\"\"\"\n\tagentStop: Permission\n}\n\n\"\"\"Autogenerated input type of AgentStop\"\"\"\ninput AgentStopInput {\n\"\"\"Autogenerated input type of AgentStop\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of AgentStop\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of AgentStop\"\"\"\n\tgraceful: Boolean\n}\n\n\"\"\"Autogenerated return type of AgentStop.\"\"\"\ntype AgentStopPayload {\n\tagent: Agent!\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n}\n\n\"\"\"A token used to connect an agent to Buildkite\"\"\"\ntype AgentToken implements Node{\n\"\"\"The time this agent token was created\"\"\"\n\tcreatedAt: DateTime\n\"\"\"The user that created this agent token\"\"\"\n\tcreatedBy: User\n\"\"\"A description about what this agent token is used for\"\"\"\n\tdescription: String\n\tid: ID!\n\torganization: Organization\n\tpermissions: AgentTokenPermissions!\n\"\"\"Whether agents registered with this token will be visible to everyone, including people outside this organization\"\"\"\n\tpublic: Boolean!\n\"\"\"The time this agent token was revoked\"\"\"\n\trevokedAt: DateTime\n\"\"\"The user that revoked this agent token\"\"\"\n\trevokedBy: User\n\"\"\"The reason as defined by the user why this token was revoked\"\"\"\n\trevokedReason: String\n\"\"\"The token value used to register a new agent\"\"\"\n\ttoken: String!\n\"\"\"The public UUID for the agent\"\"\"\n\tuuid: ID!\n}\n\ntype AgentTokenConnection implements Connection{\n\tcount: Int!\n\tedges: [AgentTokenEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of AgentTokenCreate\"\"\"\ninput AgentTokenCreateInput {\n\"\"\"Autogenerated input type of AgentTokenCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of AgentTokenCreate\"\"\"\n\torganizationID: ID!\n\"\"\"Autogenerated input type of AgentTokenCreate\"\"\"\n\tdescription: String\n\"\"\"Autogenerated input type of AgentTokenCreate\"\"\"\n\tpublic: Boolean\n}\n\n\"\"\"Autogenerated return type of AgentTokenCreate.\"\"\"\ntype AgentTokenCreatePayload {\n\tagentTokenEdge: AgentTokenEdge!\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\torganization: Organization!\n\"\"\"The token value used to register a new unclustered agent. Please ensure to securely copy this value immediately upon generation as it will not be displayed again.\"\"\"\n\ttokenValue: String!\n}\n\ntype AgentTokenEdge {\n\tcursor: String!\n\tnode: AgentToken\n}\n\n\"\"\"Permissions information about what actions the current user can do against the agent token\"\"\"\ntype AgentTokenPermissions {\n\"\"\"Whether the user can revoke this agent token\"\"\"\n\tagentTokenRevoke: Permission\n}\n\n\"\"\"Autogenerated input type of AgentTokenRevoke\"\"\"\ninput AgentTokenRevokeInput {\n\"\"\"Autogenerated input type of AgentTokenRevoke\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of AgentTokenRevoke\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of AgentTokenRevoke\"\"\"\n\treason: String!\n}\n\n\"\"\"Autogenerated return type of AgentTokenRevoke.\"\"\"\ntype AgentTokenRevokePayload {\n\tagentToken: AgentToken!\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n}\n\n\"\"\"An annotation allows you to add arbitrary content to the top of a build page in the Buildkite UI\"\"\"\ntype Annotation implements Node{\n\"\"\"The body of the annotation\"\"\"\n\tbody: AnnotationBody\n\"\"\"The context of the annotation that helps you differentiate this one from others\"\"\"\n\tcontext: String!\n\"\"\"The date the annotation was created\"\"\"\n\tcreatedAt: DateTime!\n\tid: ID!\n\"\"\"The priority of the annotation\"\"\"\n\tpriority: Int!\n\"\"\"The visual style of the annotation\"\"\"\n\tstyle: AnnotationStyle\n\"\"\"The last time the annotation was changed\"\"\"\n\tupdatedAt: DateTime\n\"\"\"The public UUID for this annotation\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"A body of an annotation\"\"\"\ntype AnnotationBody {\n\"\"\"The body of the annotation rendered as HTML. The renderer result could be an empty string if the textual version has unsupported HTML tags\"\"\"\n\thtml: String\n\"\"\"The body of the annotation as text\"\"\"\n\ttext: String!\n}\n\ntype AnnotationConnection implements Connection{\n\tcount: Int!\n\tedges: [AnnotationEdge]\n\tpageInfo: PageInfo\n}\n\ntype AnnotationEdge {\n\tcursor: String!\n\tnode: Annotation\n}\n\n\"\"\"The different orders you can sort annotations by\"\"\"\nenum AnnotationOrder {\n\"\"\"Order by priority, then by the most recently created annotations first\"\"\"\n\tPRIORITY_RECENTLY_CREATED\n\"\"\"Order by the most recently created annotations first\"\"\"\n\tRECENTLY_CREATED\n}\n\n\"\"\"The visual style of the annotation\"\"\"\nenum AnnotationStyle {\n\"\"\"The default styling of an annotation\"\"\"\n\tDEFAULT\n\"\"\"The annotation has a green border with a tick next to it\"\"\"\n\tSUCCESS\n\"\"\"The annotation has a blue border with an information icon next to it\"\"\"\n\tINFO\n\"\"\"The annotation has an orange border with a warning icon next to it\"\"\"\n\tWARNING\n\"\"\" The annotation has a red border with a cross next to it\"\"\"\n\tERROR\n}\n\n\"\"\"A file uploaded from the agent whilst running a job\"\"\"\ntype Artifact implements Node{\n\"\"\"The download URL for the artifact. Unless you've used your own artifact storage, the URL will be valid for only 10 minutes.\"\"\"\n\tdownloadURL: String!\n\"\"\"The time when the artifact will, or did, expire\"\"\"\n\texpiresAt: DateTime\n\tid: ID!\n\"\"\"The job that uploaded this artifact\"\"\"\n\tjob: JobTypeCommand\n\"\"\"The mime type of the file provided by the agent\"\"\"\n\tmimeType: String!\n\"\"\"The path of the uploaded artifact\"\"\"\n\tpath: String!\n\"\"\"A SHA1SUM of the file\"\"\"\n\tsha1sum: String!\n\"\"\"A SHA256SUM of the file\"\"\"\n\tsha256sum: String\n\"\"\"The size of the file in bytes that was uploaded\"\"\"\n\tsize: Int!\n\"\"\"The upload state of the artifact\"\"\"\n\tstate: String!\n\"\"\"The public UUID for this artifact\"\"\"\n\tuuid: ID!\n}\n\ntype ArtifactConnection implements Connection{\n\tcount: Int!\n\tedges: [ArtifactEdge]\n\tpageInfo: PageInfo\n}\n\ntype ArtifactEdge {\n\tcursor: String!\n\tnode: Artifact\n}\n\n\"\"\"Context for an audit event created during an REST/GraphQL API request\"\"\"\ntype AuditAPIContext {\n\"\"\"The API access token UUID used to authenticate the request\"\"\"\n\trequestApiAccessTokenUuid: String\n\"\"\"The remote IP which made the request\"\"\"\n\trequestIpAddress: String\n\"\"\"The client supplied user agent which made the request\"\"\"\n\trequestUserAgent: String\n}\n\n\"\"\"The actor who caused an AuditEvent\"\"\"\ntype AuditActor {\n\"\"\"The GraphQL ID for this actor\"\"\"\n\tid: ID!\n\"\"\"The name or short description of this actor\"\"\"\n\tname: String\n\"\"\"The node corresponding to this actor, if available\"\"\"\n\tnode: AuditActorNode\n\"\"\"The type of this actor\"\"\"\n\ttype: AuditActorType\n\"\"\"The public UUID of this actor\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"Kinds of actors which can perform audit events\"\"\"\nunion AuditActorNode =Agent | User\n\n\"\"\"All the possible types of actors in an Audit Event\"\"\"\nenum AuditActorType {\n\tAGENT\n\tUSER\n}\n\n\"\"\"Context for an audit event created during an agent API request\"\"\"\ntype AuditAgentAPIContext {\n\"\"\"The agent UUID\"\"\"\n\tagentUuid: String\n\"\"\"The type of token that authenticated the agent\"\"\"\n\tauthenticationType: String\n\"\"\"The connection state of the agent\"\"\"\n\tconnectionState: String\n\"\"\"The organization UUID that the agent belongs to\"\"\"\n\torganizationUuid: String\n\"\"\"The remote IP which made the request\"\"\"\n\trequestIpAddress: String\n\"\"\"The IP of the agent session which made the request\"\"\"\n\tsessionIpAddress: String\n}\n\n\"\"\"Kinds of contexts in which an audit event can be performed\"\"\"\nunion AuditContext =AuditAPIContext | AuditAgentAPIContext | AuditWebContext\n\n\"\"\"Audit record of an event which occurred in the system\"\"\"\ntype AuditEvent implements Node{\n\"\"\"The actor who caused this event\"\"\"\n\tactor: AuditActor\n\"\"\"The context in which this event occurred\"\"\"\n\tcontext: AuditContext\n\"\"\"The changed data in the event\"\"\"\n\tdata: JSON\n\tid: ID!\n\"\"\"The time at which this event occurred\"\"\"\n\toccurredAt: DateTime!\n\"\"\"The subject of this event\"\"\"\n\tsubject: AuditSubject\n\"\"\"The type of event\"\"\"\n\ttype: AuditEventType!\n\"\"\"The public UUID for the event\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"All the possible types of an Audit Event\"\"\"\nenum AuditEventType {\n\tAPI_ACCESS_TOKEN_CREATED\n\tAPI_ACCESS_TOKEN_DELETED\n\tAPI_ACCESS_TOKEN_ORGANIZATION_ACCESS_REVOKED\n\tAPI_ACCESS_TOKEN_UPDATED\n\tAGENT_TOKEN_CREATED\n\tAGENT_TOKEN_REVOKED\n\tAGENT_TOKEN_UPDATED\n\tAUTHORIZATION_CREATED\n\tAUTHORIZATION_DELETED\n\tCLUSTER_CREATED\n\tCLUSTER_DELETED\n\tCLUSTER_PERMISSION_CREATED\n\tCLUSTER_PERMISSION_DELETED\n\tCLUSTER_QUEUE_CREATED\n\tCLUSTER_QUEUE_DELETED\n\tCLUSTER_QUEUE_TOKEN_CREATED\n\tCLUSTER_QUEUE_TOKEN_DELETED\n\tCLUSTER_QUEUE_TOKEN_UPDATED\n\tCLUSTER_QUEUE_UPDATED\n\tCLUSTER_TOKEN_CREATED\n\tCLUSTER_TOKEN_DELETED\n\tCLUSTER_TOKEN_UPDATED\n\tCLUSTER_UPDATED\n\tNOTIFICATION_SERVICE_BROKEN\n\tNOTIFICATION_SERVICE_CREATED\n\tNOTIFICATION_SERVICE_DELETED\n\tNOTIFICATION_SERVICE_DISABLED\n\tNOTIFICATION_SERVICE_ENABLED\n\tNOTIFICATION_SERVICE_UPDATED\n\tORGANIZATION_BANNER_CREATED\n\tORGANIZATION_BANNER_DELETED\n\tORGANIZATION_BANNER_UPDATED\n\tORGANIZATION_BUILD_EXPORT_UPDATED\n\tORGANIZATION_CREATED\n\tORGANIZATION_DELETED\n\tORGANIZATION_INVITATION_ACCEPTED\n\tORGANIZATION_INVITATION_CREATED\n\tORGANIZATION_INVITATION_RESENT\n\tORGANIZATION_INVITATION_REVOKED\n\tORGANIZATION_MEMBER_CREATED\n\tORGANIZATION_MEMBER_DELETED\n\tORGANIZATION_MEMBER_UPDATED\n\tORGANIZATION_TEAMS_DISABLED\n\tORGANIZATION_TEAMS_ENABLED\n\tORGANIZATION_UPDATED\n\tPIPELINE_CREATED\n\tPIPELINE_DELETED\n\tPIPELINE_SCHEDULE_CREATED\n\tPIPELINE_SCHEDULE_DELETED\n\tPIPELINE_SCHEDULE_UPDATED\n\tPIPELINE_TEMPLATE_CREATED\n\tPIPELINE_TEMPLATE_DELETED\n\tPIPELINE_TEMPLATE_UPDATED\n\tPIPELINE_UPDATED\n\tPIPELINE_VISIBILITY_CHANGED\n\tPIPELINE_WEBHOOK_URL_ROTATED\n\tSCM_PIPELINE_SETTINGS_CREATED\n\tSCM_PIPELINE_SETTINGS_DELETED\n\tSCM_PIPELINE_SETTINGS_UPDATED\n\tSCM_REPOSITORY_HOST_CREATED\n\tSCM_REPOSITORY_HOST_DESTROYED\n\tSCM_REPOSITORY_HOST_UPDATED\n\tSCM_SERVICE_CREATED\n\tSCM_SERVICE_DELETED\n\tSCM_SERVICE_UPDATED\n\tSSO_PROVIDER_CREATED\n\tSSO_PROVIDER_DELETED\n\tSSO_PROVIDER_DISABLED\n\tSSO_PROVIDER_ENABLED\n\tSSO_PROVIDER_UPDATED\n\tSECRET_CREATED\n\tSECRET_DELETED\n\tSECRET_QUERIED\n\tSECRET_READ\n\tSECRET_UPDATED\n\tSUBSCRIPTION_PLAN_ADDED\n\tSUBSCRIPTION_PLAN_CHANGE_SCHEDULED\n\tSUBSCRIPTION_PLAN_CHANGED\n\tSUITE_API_TOKEN_REGENERATED\n\tSUITE_CREATED\n\tSUITE_DELETED\n\tSUITE_MONITOR_CREATED\n\tSUITE_MONITOR_DELETED\n\tSUITE_MONITOR_UPDATED\n\tSUITE_UPDATED\n\tSUITE_VISIBILITY_CHANGED\n\tTEAM_CREATED\n\tTEAM_DELETED\n\tTEAM_MEMBER_CREATED\n\tTEAM_MEMBER_DELETED\n\tTEAM_MEMBER_UPDATED\n\tTEAM_PIPELINE_CREATED\n\tTEAM_PIPELINE_DELETED\n\tTEAM_PIPELINE_UPDATED\n\tTEAM_SUITE_CREATED\n\tTEAM_SUITE_DELETED\n\tTEAM_SUITE_UPDATED\n\tTEAM_UPDATED\n\tUSER_API_ACCESS_TOKEN_ORGANIZATION_ACCESS_ADDED\n\tUSER_API_ACCESS_TOKEN_ORGANIZATION_ACCESS_REMOVED\n\tUSER_EMAIL_CREATED\n\tUSER_EMAIL_DELETED\n\tUSER_EMAIL_MARKED_PRIMARY\n\tUSER_EMAIL_VERIFIED\n\tUSER_PASSWORD_RESET\n\tUSER_PASSWORD_RESET_REQUESTED\n\tUSER_TOTP_ACTIVATED\n\tUSER_TOTP_CREATED\n\tUSER_TOTP_DELETED\n\tUSER_UPDATED\n}\n\n\"\"\"The subject of an AuditEvent\"\"\"\ntype AuditSubject {\n\"\"\"The GraphQL ID for the subject\"\"\"\n\tid: ID!\n\"\"\"The name or short description of this subject\"\"\"\n\tname: String\n\"\"\"The node corresponding to the subject, if available\"\"\"\n\tnode: AuditSubjectNode\n\"\"\"The type of this subject\"\"\"\n\ttype: AuditSubjectType\n\"\"\"The public UUID of this subject\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"Kinds of subjects which can have audit events performed on them\"\"\"\nunion AuditSubjectNode =APIAccessToken | AgentToken | AuthorizationBitbucket | AuthorizationGitHub | AuthorizationGitHubEnterprise | Cluster | ClusterPermission | ClusterQueue | ClusterQueueToken | ClusterToken | Email | NotificationServiceSlack | NotificationServiceWebhook | Organization | OrganizationBanner | OrganizationInvitation | OrganizationMember | Pipeline | PipelineSchedule | PipelineTemplate | SCMPipelineSettings | SCMRepositoryHost | SCMService | SSOProviderGitHubApp | SSOProviderGoogleGSuite | SSOProviderSAML | Secret | Subscription | Suite | TOTP | Team | TeamMember | TeamPipeline | TeamSuite | User\n\n\"\"\"All the possible types of subjects in an Audit Event\"\"\"\nenum AuditSubjectType {\n\tUSER_TOTP\n\tCLUSTER_PERMISSION\n\tUSER\n\tCLUSTER\n\tSECRET\n\tORGANIZATION_MEMBER\n\tORGANIZATION\n\tPIPELINE\n\tTEAM\n\tSSO_PROVIDER\n\tSUITE\n\tSUBSCRIPTION\n\tAUTHORIZATION\n\tAGENT_TOKEN\n\tAPI_ACCESS_TOKEN\n\tCLUSTER_QUEUE\n\tCLUSTER_TOKEN\n\tCLUSTER_QUEUE_TOKEN\n\tNOTIFICATION_SERVICE\n\tORGANIZATION_BANNER\n\tORGANIZATION_INVITATION\n\tPIPELINE_SCHEDULE\n\tPIPELINE_TEMPLATE\n\tTEAM_MEMBER\n\tTEAM_PIPELINE\n\tTEAM_SUITE\n\tSCM_SERVICE\n\tSCM_PIPELINE_SETTINGS\n\tSCM_REPOSITORY_HOST\n\tSUITE_MONITOR\n\tUSER_EMAIL\n}\n\n\"\"\"Context for an audit event created during a web request\"\"\"\ntype AuditWebContext {\n\"\"\"The remote IP which made the request\"\"\"\n\trequestIpAddress: String\n\"\"\"The client supplied user agent which made the request\"\"\"\n\trequestUserAgent: String\n\"\"\"When the session started, if available\"\"\"\n\tsessionCreatedAt: DateTime\n\"\"\"When the session was escalated, if available and escalated\"\"\"\n\tsessionEscalatedAt: DateTime\n\"\"\"The session's authenticated user, if available\"\"\"\n\tsessionUser: User\n\"\"\"The session's authenticated user's uuid\"\"\"\n\tsessionUserUuid: ID\n}\n\ninterface Authorization {\n\tid: ID!\n}\n\n\"\"\"A Bitbucket account authorized with a Buildkite account\"\"\"\ntype AuthorizationBitbucket implements Authorization & Node{\n\"\"\"ID of the object.\"\"\"\n\tid: ID!\n}\n\ntype AuthorizationConnection implements Connection{\n\tcount: Int!\n\tedges: [AuthorizationEdge]\n\tpageInfo: PageInfo\n}\n\ntype AuthorizationEdge {\n\tcursor: String!\n\tnode: Authorization\n}\n\n\"\"\"A GitHub account authorized with a Buildkite account\"\"\"\ntype AuthorizationGitHub implements Authorization & Node{\n\"\"\"ID of the object.\"\"\"\n\tid: ID!\n}\n\n\"\"\"A GitHub app authorized with a Buildkite account\"\"\"\ntype AuthorizationGitHubApp implements Authorization & Node{\n\"\"\"ID of the object.\"\"\"\n\tid: ID!\n}\n\n\"\"\"A GitHub Enterprise account authorized with a Buildkite account\"\"\"\ntype AuthorizationGitHubEnterprise implements Authorization & Node{\n\"\"\"ID of the object.\"\"\"\n\tid: ID!\n}\n\n\"\"\"A Google account authorized with a Buildkite account\"\"\"\ntype AuthorizationGoogle implements Authorization & Node{\n\"\"\"ID of the object.\"\"\"\n\tid: ID!\n}\n\n\"\"\"A SAML account authorized with a Buildkite account\"\"\"\ntype AuthorizationSAML implements Authorization & Node{\n\"\"\"ID of the object.\"\"\"\n\tid: ID!\n}\n\n\"\"\"The type of the authorization\"\"\"\nenum AuthorizationType {\n\"\"\"GitHub Authorization\"\"\"\n\tGITHUB\n\"\"\"GitHub Enterprise Authorization\"\"\"\n\tGITHUB_ENTERPRISE\n\"\"\"Bitbucket Authorization\"\"\"\n\tBITBUCKET\n}\n\n\"\"\"An avatar belonging to a user\"\"\"\ntype Avatar {\n\"\"\"The URL of the avatar\"\"\"\n\turl: String!\n}\n\n\"\"\"Represents `true` or `false` values.\"\"\"\nscalar Boolean\n\n\"\"\"A build from a pipeline\"\"\"\ntype Build implements Node{\n\tannotations(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t\tstyle: [AnnotationStyle!]\n\"\"\"Order the annotations\"\"\"\n\t\torder: AnnotationOrder\n\t): AnnotationConnection\n\"\"\"The current blocked state of the build\"\"\"\n\tblockedState: BuildBlockedStates\n\"\"\"The branch for the build\"\"\"\n\tbranch: String!\n\"\"\"The time when the build was cancelled\"\"\"\n\tcanceledAt: DateTime\n\"\"\"The user who canceled this build. If the build was canceled, and this value is null, then it was canceled automatically by Buildkite\"\"\"\n\tcanceledBy: User\n\"\"\"The fully-qualified commit for the build\"\"\"\n\tcommit: String!\n\"\"\"The time when the build was created\"\"\"\n\tcreatedAt: DateTime\n\tcreatedBy: BuildCreator\n\"\"\"Custom environment variables passed to this build\"\"\"\n\tenv: [String!]\n\"\"\"The time when the build finished\"\"\"\n\tfinishedAt: DateTime\n\tid: ID!\n\tjobs(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t\ttype: [JobTypes!]\n\t\tstate: [JobStates!]\n\t\tpriority: JobPrioritySearch\n\t\tagentQueryRules: [String!]\n\t\tconcurrency: JobConcurrencySearch\n\"\"\"Whether or not the command job passed. Passing `false` will return all failed jobs (including \"soft failed\" jobs)\"\"\"\n\t\tpassed: Boolean\n\"\"\"Filtering jobs based on related step information\"\"\"\n\t\tstep: JobStepSearch\n\"\"\"Order the jobs\"\"\"\n\t\torder: JobOrder\n\t): JobConnection\n\"\"\"The message for the build\"\"\"\n\tmessage: String\n\tmetaData(\n\t\tfirst: Int\n\t\tlast: Int\n\t): BuildMetaDataConnection\n\"\"\"The number of the build\"\"\"\n\tnumber: Int!\n\torganization: Organization!\n\tpipeline: Pipeline!\n\tpullRequest: PullRequest\n\"\"\"The build that this build was rebuilt from\"\"\"\n\trebuiltFrom: Build\n\"\"\"The time when the build became scheduled for running\"\"\"\n\tscheduledAt: DateTime\n\"\"\"Where the build was created\"\"\"\n\tsource: BuildSource!\n\"\"\"The time when the build started running\"\"\"\n\tstartedAt: DateTime\n\"\"\"The current state of the build\"\"\"\n\tstate: BuildStates!\n\"\"\"The job that this build was triggered from\"\"\"\n\ttriggeredFrom: JobTypeTrigger\n\"\"\"The URL for the build\"\"\"\n\turl: String!\n\"\"\"The UUID for the build\"\"\"\n\tuuid: String!\n}\n\n\"\"\"Autogenerated input type of BuildAnnotate\"\"\"\ninput BuildAnnotateInput {\n\"\"\"Autogenerated input type of BuildAnnotate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of BuildAnnotate\"\"\"\n\tbuildID: ID!\n\"\"\"Autogenerated input type of BuildAnnotate\"\"\"\n\tbody: String\n\"\"\"Autogenerated input type of BuildAnnotate\"\"\"\n\tstyle: AnnotationStyle\n\"\"\"Autogenerated input type of BuildAnnotate\"\"\"\n\tcontext: String\n\"\"\"Autogenerated input type of BuildAnnotate\"\"\"\n\tappend: Boolean\n\"\"\"Autogenerated input type of BuildAnnotate\"\"\"\n\tpriority: Int\n}\n\n\"\"\"Autogenerated return type of BuildAnnotate.\"\"\"\ntype BuildAnnotatePayload {\n\tannotation: Annotation\n\tannotationEdge: AnnotationEdge\n\tbuild: Build\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n}\n\n\"\"\"Author for a build\"\"\"\ninput BuildAuthorInput {\n\"\"\"Author for a build\"\"\"\n\tname: String!\n\"\"\"Author for a build\"\"\"\n\temail: String!\n}\n\n\"\"\"All the possible blocked states a build can be in\"\"\"\nenum BuildBlockedStates {\n\"\"\"The blocked build is running\"\"\"\n\tRUNNING\n\"\"\"The blocked build is passed\"\"\"\n\tPASSED\n\"\"\"The blocked build is failed\"\"\"\n\tFAILED\n}\n\n\"\"\"Autogenerated input type of BuildCancel\"\"\"\ninput BuildCancelInput {\n\"\"\"Autogenerated input type of BuildCancel\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of BuildCancel\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of BuildCancel.\"\"\"\ntype BuildCancelPayload {\n\tbuild: Build!\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n}\n\ntype BuildConnection implements Connection{\n\tcount: Int!\n\tedges: [BuildEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of BuildCreate\"\"\"\ninput BuildCreateInput {\n\"\"\"Autogenerated input type of BuildCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of BuildCreate\"\"\"\n\tpipelineID: ID!\n\"\"\"Autogenerated input type of BuildCreate\"\"\"\n\tmessage: String\n\"\"\"Autogenerated input type of BuildCreate\"\"\"\n\tcommit: String\n\"\"\"Autogenerated input type of BuildCreate\"\"\"\n\tbranch: String\n\"\"\"Autogenerated input type of BuildCreate\"\"\"\n\tenv: [String!]\n\"\"\"Autogenerated input type of BuildCreate\"\"\"\n\tmetaData: [BuildMetaDataInput!]\n\"\"\"Autogenerated input type of BuildCreate\"\"\"\n\tauthor: BuildAuthorInput\n}\n\n\"\"\"Autogenerated return type of BuildCreate.\"\"\"\ntype BuildCreatePayload {\n\tbuild: Build\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n}\n\n\"\"\"Either a `User` or an `UnregisteredUser` type\"\"\"\nunion BuildCreator =UnregisteredUser | User\n\ntype BuildEdge {\n\tcursor: String!\n\tnode: Build\n}\n\n\"\"\"A comment on a build\"\"\"\ntype BuildMetaData {\n\"\"\"The key used to set this meta data\"\"\"\n\tkey: String!\n\"\"\"The value set to this meta data\"\"\"\n\tvalue: String!\n}\n\ntype BuildMetaDataConnection implements Connection{\n\tcount: Int!\n\tedges: [BuildMetaDataEdge]\n\tpageInfo: PageInfo\n}\n\ntype BuildMetaDataEdge {\n\tcursor: String!\n\tnode: BuildMetaData\n}\n\n\"\"\"Meta-data key/value pairs for a build\"\"\"\ninput BuildMetaDataInput {\n\"\"\"Meta-data key/value pairs for a build\"\"\"\n\tkey: String!\n\"\"\"Meta-data key/value pairs for a build\"\"\"\n\tvalue: String!\n}\n\n\"\"\"Autogenerated input type of BuildRebuild\"\"\"\ninput BuildRebuildInput {\n\"\"\"Autogenerated input type of BuildRebuild\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of BuildRebuild\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of BuildRebuild.\"\"\"\ntype BuildRebuildPayload {\n\tbuild: Build!\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\trebuild: Build!\n}\n\ninterface BuildSource {\n\tname: String!\n}\n\n\"\"\"A build was triggered via an API\"\"\"\ntype BuildSourceAPI implements BuildSource{\n\tname: String!\n}\n\n\"\"\"A build was triggered manually via the frontend\"\"\"\ntype BuildSourceFrontend implements BuildSource{\n\tname: String!\n}\n\n\"\"\"A build was triggered via a schedule\"\"\"\ntype BuildSourceSchedule implements BuildSource{\n\tname: String!\n\"\"\"The associated schedule that created this build. Will be `null` if the associated schedule has been deleted.\"\"\"\n\tpipelineSchedule: PipelineSchedule\n}\n\n\"\"\"A build was triggered via a trigger job\"\"\"\ntype BuildSourceTriggerJob implements BuildSource{\n\tname: String!\n}\n\n\"\"\"A build was triggered via a Webhook\"\"\"\ntype BuildSourceWebhook implements BuildSource{\n\"\"\"Provider specific headers sent along with the webhook. This will return null if the webhook has been purged by Buildkite.\"\"\"\n\theaders: [String!]\n\tname: String!\n\"\"\"The body of the webhook. Buildkite only stores webhook data for a short period of time, so if this returns null - then the webhook data has been purged by Buildkite\"\"\"\n\tpayload: JSON\n\"\"\"The UUID for this webhook. This will return null if the webhook has been purged by Buildkite\"\"\"\n\tuuid: String\n}\n\n\"\"\"All the possible states a build can be in\"\"\"\nenum BuildStates {\n\"\"\"The build was skipped\"\"\"\n\tSKIPPED\n\"\"\"The build is currently being created\"\"\"\n\tCREATING\n\"\"\"The build has yet to start running jobs\"\"\"\n\tSCHEDULED\n\"\"\"The build is currently running jobs\"\"\"\n\tRUNNING\n\"\"\"The build passed\"\"\"\n\tPASSED\n\"\"\"The build failed\"\"\"\n\tFAILED\n\"\"\"The build is failing\"\"\"\n\tFAILING\n\"\"\"The build is currently being canceled\"\"\"\n\tCANCELING\n\"\"\"The build was canceled\"\"\"\n\tCANCELED\n\"\"\"The build is blocked\"\"\"\n\tBLOCKED\n\"\"\"The build wasn't run\"\"\"\n\tNOT_RUN\n}\n\n\"\"\"The results of a `buildkite-agent pipeline upload`\"\"\"\ntype BuildStepUpload {\n\"\"\"The uploaded step definition\"\"\"\n\tdefinition: BuildStepUploadDefinition!\n\tid: ID!\n\"\"\"The UUID for this build step upload\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"The pipeline definition for a step upload\"\"\"\ntype BuildStepUploadDefinition {\n\"\"\"The uploaded step definition rendered as JSON\"\"\"\n\tjson: String!\n\"\"\"The uploaded step definition rendered as YAML\"\"\"\n\tyaml: String!\n}\n\n\"\"\"A changelog\"\"\"\ntype Changelog implements Node{\n\tauthor: ChangelogAuthor\n\"\"\"The body of this changelog\"\"\"\n\tbody: String\n\tid: ID!\n\"\"\"The date and time this changelog was published\"\"\"\n\tpublishedAt: DateTime\n\"\"\"The tag for this changelog\"\"\"\n\ttag: String!\n\"\"\"The title for this changelog\"\"\"\n\ttitle: String!\n\"\"\"The public UUID for this changelog\"\"\"\n\tuuid: String!\n}\n\n\"\"\"The author of the changelog\"\"\"\ntype ChangelogAuthor {\n\tavatar: Avatar!\n\"\"\"The name of the author\"\"\"\n\tname: String!\n}\n\ntype ChangelogConnection implements Connection{\n\tcount: Int!\n\tedges: [ChangelogEdge]\n\tpageInfo: PageInfo\n}\n\ntype ChangelogEdge {\n\tcursor: String!\n\tnode: Changelog\n}\n\ntype Cluster implements Node{\n\"\"\"Returns agent tokens for the Cluster\"\"\"\n\tagentTokens(\n\t\tfirst: Int\n\t\tlast: Int\n\t): ClusterAgentTokenConnection\n\"\"\"Color hex code for the cluster\"\"\"\n\tcolor: String\n\"\"\"User who created the cluster\"\"\"\n\tcreatedBy: User\n\"\"\"The default queue that agents connecting to the cluster without specifying a queue will accept jobs from\"\"\"\n\tdefaultQueue: ClusterQueue\n\"\"\"Description of the cluster\"\"\"\n\tdescription: String\n\"\"\"Emoji for the cluster using Buildkite emoji syntax\"\"\"\n\temoji: String\n\tid: ID!\n\"\"\"Name of the cluster\"\"\"\n\tname: String!\n\torganization: Organization\n\tqueues(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\"\"\"Order the cluster queues\"\"\"\n\t\torder: ClusterQueueOrder\n\t): ClusterQueueConnection\n\"\"\"The public UUID for this cluster\"\"\"\n\tuuid: ID!\n}\n\ntype ClusterAgentTokenConnection implements Connection{\n\tcount: Int!\n\tedges: [ClusterAgentTokenEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of ClusterAgentTokenCreate\"\"\"\ninput ClusterAgentTokenCreateInput {\n\"\"\"Autogenerated input type of ClusterAgentTokenCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of ClusterAgentTokenCreate\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of ClusterAgentTokenCreate\"\"\"\n\tdescription: String!\n\"\"\"Autogenerated input type of ClusterAgentTokenCreate\"\"\"\n\tclusterId: ID!\n\"\"\"Autogenerated input type of ClusterAgentTokenCreate\"\"\"\n\tallowedIpAddresses: String\n}\n\n\"\"\"Autogenerated return type of ClusterAgentTokenCreate.\"\"\"\ntype ClusterAgentTokenCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tclusterAgentToken: ClusterToken!\n\"\"\"The token value used to register a new agent token to this cluster. Please ensure to securely copy this value immediately upon generation as it will not be displayed again.\"\"\"\n\ttokenValue: String!\n}\n\ntype ClusterAgentTokenEdge {\n\tcursor: String!\n\tnode: ClusterToken\n}\n\n\"\"\"Autogenerated input type of ClusterAgentTokenRevoke\"\"\"\ninput ClusterAgentTokenRevokeInput {\n\"\"\"Autogenerated input type of ClusterAgentTokenRevoke\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of ClusterAgentTokenRevoke\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of ClusterAgentTokenRevoke\"\"\"\n\torganizationId: ID!\n}\n\n\"\"\"Autogenerated return type of ClusterAgentTokenRevoke.\"\"\"\ntype ClusterAgentTokenRevokePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedClusterAgentTokenId: ID!\n}\n\n\"\"\"Autogenerated input type of ClusterAgentTokenUpdate\"\"\"\ninput ClusterAgentTokenUpdateInput {\n\"\"\"Autogenerated input type of ClusterAgentTokenUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of ClusterAgentTokenUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of ClusterAgentTokenUpdate\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of ClusterAgentTokenUpdate\"\"\"\n\tdescription: String!\n\"\"\"Autogenerated input type of ClusterAgentTokenUpdate\"\"\"\n\tallowedIpAddresses: String\n}\n\n\"\"\"Autogenerated return type of ClusterAgentTokenUpdate.\"\"\"\ntype ClusterAgentTokenUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tclusterAgentToken: ClusterToken!\n}\n\ntype ClusterConnection implements Connection{\n\tcount: Int!\n\tedges: [ClusterEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of ClusterCreate\"\"\"\ninput ClusterCreateInput {\n\"\"\"Autogenerated input type of ClusterCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of ClusterCreate\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of ClusterCreate\"\"\"\n\tname: String!\n\"\"\"Autogenerated input type of ClusterCreate\"\"\"\n\tdescription: String\n\"\"\"Autogenerated input type of ClusterCreate\"\"\"\n\temoji: String\n\"\"\"Autogenerated input type of ClusterCreate\"\"\"\n\tcolor: String\n}\n\n\"\"\"Autogenerated return type of ClusterCreate.\"\"\"\ntype ClusterCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tcluster: Cluster!\n}\n\n\"\"\"Autogenerated input type of ClusterDelete\"\"\"\ninput ClusterDeleteInput {\n\"\"\"Autogenerated input type of ClusterDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of ClusterDelete\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of ClusterDelete\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of ClusterDelete.\"\"\"\ntype ClusterDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedClusterId: ID!\n}\n\ntype ClusterEdge {\n\tcursor: String!\n\tnode: Cluster\n}\n\n\"\"\"The different orders you can sort clusters by\"\"\"\nenum ClusterOrder {\n\"\"\"Order by name alphabetically\"\"\"\n\tNAME\n\"\"\"Order by the most recently created clusters first\"\"\"\n\tRECENTLY_CREATED\n}\n\ntype ClusterPermission {\n\tactor: ClusterPermissionActor\n\"\"\"Whether the actor can add pipelines to this cluster\"\"\"\n\tcan_add_pipelines: Boolean!\n\"\"\"Whether the actor can manage the associated cluster\"\"\"\n\tcan_manage: Boolean!\n\"\"\"Whether the actor can see this cluster's tokens\"\"\"\n\tcan_see_tokens: Boolean!\n\tcluster: Cluster\n\tid: ID!\n\"\"\"The public UUID for this cluster permission\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"Actor to whom a cluster permission is applied\"\"\"\nunion ClusterPermissionActor =OrganizationMember | Team\n\ntype ClusterQueue implements Node{\n\tcluster: Cluster\n\tcreatedBy: User\n\tdescription: String\n\"\"\"States whether job dispatch is paused for this cluster queue\"\"\"\n\tdispatchPaused: Boolean!\n\"\"\"The time this queue was paused\"\"\"\n\tdispatchPausedAt: DateTime\n\"\"\"The user who paused this cluster queue\"\"\"\n\tdispatchPausedBy: User\n\"\"\"Note describing why job dispatch was paused for this cluster queue\"\"\"\n\tdispatchPausedNote: String\n\tid: ID!\n\tkey: String!\n\"\"\"The public UUID for this cluster queue\"\"\"\n\tuuid: ID!\n}\n\ntype ClusterQueueConnection implements Connection{\n\tcount: Int!\n\tedges: [ClusterQueueEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of ClusterQueueCreate\"\"\"\ninput ClusterQueueCreateInput {\n\"\"\"Autogenerated input type of ClusterQueueCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of ClusterQueueCreate\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of ClusterQueueCreate\"\"\"\n\tclusterId: ID!\n\"\"\"Autogenerated input type of ClusterQueueCreate\"\"\"\n\tkey: String!\n\"\"\"Autogenerated input type of ClusterQueueCreate\"\"\"\n\tdescription: String\n}\n\n\"\"\"Autogenerated return type of ClusterQueueCreate.\"\"\"\ntype ClusterQueueCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tclusterQueue: ClusterQueue!\n}\n\n\"\"\"Autogenerated input type of ClusterQueueDelete\"\"\"\ninput ClusterQueueDeleteInput {\n\"\"\"Autogenerated input type of ClusterQueueDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of ClusterQueueDelete\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of ClusterQueueDelete\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of ClusterQueueDelete.\"\"\"\ntype ClusterQueueDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedClusterQueueId: ID!\n}\n\ntype ClusterQueueEdge {\n\tcursor: String!\n\tnode: ClusterQueue\n}\n\n\"\"\"The different orders you can sort cluster queues by\"\"\"\nenum ClusterQueueOrder {\n\"\"\"Order by key alphabetically\"\"\"\n\tKEY\n\"\"\"Order by the most recently created cluster queues first\"\"\"\n\tRECENTLY_CREATED\n}\n\n\"\"\"Autogenerated input type of ClusterQueuePauseDispatch\"\"\"\ninput ClusterQueuePauseDispatchInput {\n\"\"\"Autogenerated input type of ClusterQueuePauseDispatch\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of ClusterQueuePauseDispatch\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of ClusterQueuePauseDispatch\"\"\"\n\tnote: String\n}\n\n\"\"\"Autogenerated return type of ClusterQueuePauseDispatch.\"\"\"\ntype ClusterQueuePauseDispatchPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tqueue: ClusterQueue!\n}\n\n\"\"\"Autogenerated input type of ClusterQueueResumeDispatch\"\"\"\ninput ClusterQueueResumeDispatchInput {\n\"\"\"Autogenerated input type of ClusterQueueResumeDispatch\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of ClusterQueueResumeDispatch\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of ClusterQueueResumeDispatch.\"\"\"\ntype ClusterQueueResumeDispatchPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tqueue: ClusterQueue!\n}\n\n\"\"\"A token used to register an agent with a Buildkite cluster queue\"\"\"\ntype ClusterQueueToken implements Node{\n\"\"\"A list of CIDR-notation IPv4 addresses from which agents can use this token. Please note that this feature is not yet available to all organizations\"\"\"\n\tallowedIpAddresses: String\n\tcluster: Cluster\n\tclusterQueue: ClusterQueue\n\tcreatedBy: User\n\"\"\"A description for this cluster queue token\"\"\"\n\tdescription: String!\n\tid: ID!\n\"\"\"The public UUID for this cluster queue token\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"Autogenerated input type of ClusterQueueUpdate\"\"\"\ninput ClusterQueueUpdateInput {\n\"\"\"Autogenerated input type of ClusterQueueUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of ClusterQueueUpdate\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of ClusterQueueUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of ClusterQueueUpdate\"\"\"\n\tdescription: String\n}\n\n\"\"\"Autogenerated return type of ClusterQueueUpdate.\"\"\"\ntype ClusterQueueUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tclusterQueue: ClusterQueue!\n}\n\n\"\"\"A token used to connect an agent in cluster to Buildkite\"\"\"\ntype ClusterToken implements Node{\n\"\"\"A list of CIDR-notation IPv4 addresses from which agents can use this token. Please note that this feature is not yet available to all organizations\"\"\"\n\tallowedIpAddresses: String\n\tcluster: Cluster\n\tcreatedBy: User\n\"\"\"A description about what this cluster agent token is used for\"\"\"\n\tdescription: String\n\tid: ID!\n\"\"\"The token value used to register a new agent to this tokens cluster. This will soon return an empty string before we finally remove this field.\"\"\"\n\ttoken: String!\n\"\"\"The public UUID for this cluster token\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"Autogenerated input type of ClusterUpdate\"\"\"\ninput ClusterUpdateInput {\n\"\"\"Autogenerated input type of ClusterUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of ClusterUpdate\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of ClusterUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of ClusterUpdate\"\"\"\n\tname: String\n\"\"\"Autogenerated input type of ClusterUpdate\"\"\"\n\tdescription: String\n\"\"\"Autogenerated input type of ClusterUpdate\"\"\"\n\temoji: String\n\"\"\"Autogenerated input type of ClusterUpdate\"\"\"\n\tcolor: String\n\"\"\"Autogenerated input type of ClusterUpdate\"\"\"\n\tdefaultQueueId: ID\n}\n\n\"\"\"Autogenerated return type of ClusterUpdate.\"\"\"\ntype ClusterUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tcluster: Cluster!\n}\n\ninterface Connection {\n\tcount: Int!\n\tpageInfo: PageInfo\n}\n\n\"\"\"An ISO-8601 encoded UTC date string\"\"\"\nscalar DateTime\n\ntype Dependency {\n\"\"\"Is this dependency allowed to fail\"\"\"\n\tallowFailure: Boolean!\n\tid: ID!\n\"\"\"The step key or step identifier that this step depends on\"\"\"\n\tkey: String\n\"\"\"The UUID for this dependency\"\"\"\n\tuuid: ID!\n}\n\ntype DependencyConnection implements Connection{\n\tcount: Int!\n\tedges: [DependencyEdge]\n\tpageInfo: PageInfo\n}\n\ntype DependencyEdge {\n\tcursor: String!\n\tnode: Dependency\n}\n\n\"\"\"A job dispatch for a particular Organization\"\"\"\ntype Dispatch {\n\tid: ID!\n\"\"\"The public UUID for this organization dispatch\"\"\"\n\tuuid: String!\n}\n\n\"\"\"An email address\"\"\"\ntype Email implements Node{\n\"\"\"The email address\"\"\"\n\taddress: String!\n\tid: ID!\n\"\"\"Whether the email address is the user's primary address\"\"\"\n\tprimary: Boolean!\n\"\"\"The public UUID for this email\"\"\"\n\tuuid: ID!\n\"\"\"Whether the email address has been verified by the user\"\"\"\n\tverified: Boolean!\n}\n\n\"\"\"The connection type for Email.\"\"\"\ntype EmailConnection implements Connection{\n\tcount: Int!\n\"\"\"A list of edges.\"\"\"\n\tedges: [EmailEdge]\n\"\"\"A list of nodes.\"\"\"\n\tnodes: [Email]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of EmailCreate\"\"\"\ninput EmailCreateInput {\n\"\"\"Autogenerated input type of EmailCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of EmailCreate\"\"\"\n\taddress: String!\n}\n\n\"\"\"Autogenerated return type of EmailCreate.\"\"\"\ntype EmailCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\temailEdge: EmailEdge!\n\tviewer: Viewer!\n}\n\n\"\"\"An edge in a connection.\"\"\"\ntype EmailEdge {\n\"\"\"A cursor for use in pagination.\"\"\"\n\tcursor: String!\n\"\"\"The item at the end of the edge.\"\"\"\n\tnode: Email\n}\n\n\"\"\"Autogenerated input type of EmailResendVerification\"\"\"\ninput EmailResendVerificationInput {\n\"\"\"Autogenerated input type of EmailResendVerification\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of EmailResendVerification\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of EmailResendVerification.\"\"\"\ntype EmailResendVerificationPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\temail: Email!\n}\n\n\"\"\"A shared GraphQL query\"\"\"\ntype GraphQLSnippet {\n\"\"\"When this GraphQL snippet was created\"\"\"\n\tcreatedAt: DateTime!\n\tid: ID!\n\"\"\"The default operation name for this snippet\"\"\"\n\toperationName: String\n\"\"\"The query of this GraphQL snippet\"\"\"\n\tquery: String!\n\"\"\"The URL for the GraphQL snippet\"\"\"\n\turl: String!\n\"\"\"The public UUID for this snippet\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"Autogenerated input type of GraphQLSnippetCreate\"\"\"\ninput GraphQLSnippetCreateInput {\n\"\"\"Autogenerated input type of GraphQLSnippetCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of GraphQLSnippetCreate\"\"\"\n\tquery: String!\n\"\"\"Autogenerated input type of GraphQLSnippetCreate\"\"\"\n\toperationName: ID\n}\n\n\"\"\"Autogenerated return type of GraphQLSnippetCreate.\"\"\"\ntype GraphQLSnippetCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tgraphQLSnippet: GraphQLSnippet!\n}\n\n\"\"\"Represents a unique identifier that is Base64 obfuscated. It is often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"VXNlci0xMA==\"`) or integer (such as `4`) input value will be accepted as an ID.\"\"\"\nscalar ID\n\n\"\"\"An ISO 8601-encoded date\"\"\"\nscalar ISO8601Date\n\n\"\"\"Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.\"\"\"\nscalar Int\n\n\"\"\"Represents non-fractional signed whole numeric values.\n\n`JSInt` can represent values between -(2^53) + 1 and 2^53 - 1.\n\"\"\"\nscalar JSInt\n\n\"\"\"A blob of JSON represented as a pretty formatted string\"\"\"\nscalar JSON\n\n\"\"\"Kinds of jobs that can exist on a build\"\"\"\nunion Job =JobTypeBlock | JobTypeCommand | JobTypeTrigger | JobTypeWait\n\n\"\"\"Concurrency configuration for a job\"\"\"\ntype JobConcurrency {\n\"\"\"The concurrency group\"\"\"\n\tgroup: String!\n\"\"\"The maximum amount of jobs in the concurrency that are allowed to run at any given time\"\"\"\n\tlimit: Int!\n}\n\n\"\"\"Searching for concurrency groups on jobs\"\"\"\ninput JobConcurrencySearch {\n\"\"\"Searching for concurrency groups on jobs\"\"\"\n\tgroup: [String!]\n}\n\ntype JobConnection implements Connection{\n\tcount: Int!\n\tedges: [JobEdge]\n\tpageInfo: PageInfo\n}\n\ntype JobEdge {\n\tcursor: String!\n\tnode: Job\n}\n\ninterface JobEvent {\n\tactor: JobEventActor!\n\tid: ID!\n\tjob: JobTypeCommand!\n\ttimestamp: DateTime!\n\ttype: JobEventType!\n\tuuid: ID!\n}\n\n\"\"\"The actor who was responsible for the job event\"\"\"\ntype JobEventActor {\n\"\"\"The node corresponding to this actor if available\"\"\"\n\tnode: JobEventActorNodeUnion\n\"\"\"The type of this actor\"\"\"\n\ttype: JobEventActorType!\n\"\"\"The public UUID of this actor if available\"\"\"\n\tuuid: ID\n}\n\n\"\"\"Actor types that can create events on a job\"\"\"\nunion JobEventActorNodeUnion =Agent | Dispatch | User\n\n\"\"\"All the actors that can have created a job event\"\"\"\nenum JobEventActorType {\n\"\"\"The actor was a user\"\"\"\n\tUSER\n\"\"\"The actor was an agent\"\"\"\n\tAGENT\n\"\"\"The actor was the system\"\"\"\n\tSYSTEM\n\"\"\"The actor was the dispatcher\"\"\"\n\tDISPATCH\n}\n\n\"\"\"An event created when the dispatcher assigns the job to an agent\"\"\"\ntype JobEventAssigned implements JobEvent & Node{\n\"\"\"The actor that caused this event to occur\"\"\"\n\tactor: JobEventActor!\n\"\"\"The agent the job was assigned to\"\"\"\n\tassignedAgent: Agent\n\tid: ID!\n\"\"\"The job that this event belongs to\"\"\"\n\tjob: JobTypeCommand!\n\"\"\"The time when the event occurred\"\"\"\n\ttimestamp: DateTime!\n\"\"\"The type of event\"\"\"\n\ttype: JobEventType!\n\"\"\"The public UUID for this job event\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"An event created when the job creates new build steps via pipeline upload\"\"\"\ntype JobEventBuildStepUploadCreated implements JobEvent & Node{\n\"\"\"The actor that caused this event to occur\"\"\"\n\tactor: JobEventActor!\n\tbuildStepUpload: BuildStepUpload!\n\tid: ID!\n\"\"\"The job that this event belongs to\"\"\"\n\tjob: JobTypeCommand!\n\"\"\"The time when the event occurred\"\"\"\n\ttimestamp: DateTime!\n\"\"\"The type of event\"\"\"\n\ttype: JobEventType!\n\"\"\"The public UUID for this job event\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"An event created when the job is canceled\"\"\"\ntype JobEventCanceled implements JobEvent & Node{\n\"\"\"The actor that caused this event to occur\"\"\"\n\tactor: JobEventActor!\n\texitStatus: JSInt!\n\tid: ID!\n\"\"\"The job that this event belongs to\"\"\"\n\tjob: JobTypeCommand!\n\"\"\"The termination signal which killed the command, if the command was killed\"\"\"\n\tsignal: String\n\"\"\"If the termination signal was sent by the agent, the reason the agent took that action. If this field is null, and the `signal` field is not null, the command was killed by another process or by the operating system.\"\"\"\n\tsignalReason: JobEventSignalReason\n\"\"\"The time when the event occurred\"\"\"\n\ttimestamp: DateTime!\n\"\"\"The type of event\"\"\"\n\ttype: JobEventType!\n\"\"\"The public UUID for this job event\"\"\"\n\tuuid: ID!\n}\n\ntype JobEventConnection implements Connection{\n\tcount: Int!\n\tedges: [JobEventEdge]\n\tpageInfo: PageInfo\n}\n\ntype JobEventEdge {\n\tcursor: String!\n\tnode: JobEvent!\n}\n\n\"\"\"An event created when the job is finished\"\"\"\ntype JobEventFinished implements JobEvent & Node{\n\"\"\"The actor that caused this event to occur\"\"\"\n\tactor: JobEventActor!\n\"\"\"The exit status returned by the command on the agent. A value of `-1` indicates either that the agent was lost or the process was killed. If the process was killed, the `signal` field will be non-null.\"\"\"\n\texitStatus: JSInt!\n\tid: ID!\n\"\"\"The job that this event belongs to\"\"\"\n\tjob: JobTypeCommand!\n\"\"\"The termination signal which killed the command, if the command was killed\"\"\"\n\tsignal: String\n\"\"\"If the termination signal was sent by the agent, the reason the agent took that action. If this field is null, and the `signal` field is not null, the command was killed by another process or by the operating system.\"\"\"\n\tsignalReason: JobEventSignalReason\n\"\"\"The time when the event occurred\"\"\"\n\ttimestamp: DateTime!\n\"\"\"The type of event\"\"\"\n\ttype: JobEventType!\n\"\"\"The public UUID for this job event\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"A generic event type that doesn't have any additional meta-information associated with the event\"\"\"\ntype JobEventGeneric implements JobEvent & Node{\n\"\"\"The actor that caused this event to occur\"\"\"\n\tactor: JobEventActor!\n\tid: ID!\n\"\"\"The job that this event belongs to\"\"\"\n\tjob: JobTypeCommand!\n\"\"\"The time when the event occurred\"\"\"\n\ttimestamp: DateTime!\n\"\"\"The type of event\"\"\"\n\ttype: JobEventType!\n\"\"\"The public UUID for this job event\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"An event created when the job is retried\"\"\"\ntype JobEventRetried implements JobEvent & Node{\n\"\"\"The actor that caused this event to occur\"\"\"\n\tactor: JobEventActor!\n\tautomaticRule: JobRetryRuleAutomatic\n\tid: ID!\n\"\"\"The job that this event belongs to\"\"\"\n\tjob: JobTypeCommand!\n\tretriedInJob: JobTypeCommand\n\"\"\"The time when the event occurred\"\"\"\n\ttimestamp: DateTime!\n\"\"\"The type of event\"\"\"\n\ttype: JobEventType!\n\"\"\"The public UUID for this job event\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"The reason why a signal was sent to the job's process, or why the process did not start\"\"\"\nenum JobEventSignalReason {\n\"\"\"The agent sent the signal to the process because the agent was stopped\"\"\"\n\tAGENT_STOP\n\"\"\"The agent sent the signal to the process because the job was canceled\"\"\"\n\tCANCEL\n\"\"\"The agent was unable to start the job process, often due to memory or resource constraints. Note that in this case, no signal was sent to the process, it simply never started.\"\"\"\n\tPROCESS_RUN_ERROR\n\"\"\"The agent refused the job. Note that in this case, no signal was sent to the process, the job was not run at all.\"\"\"\n\tAGENT_REFUSED\n\"\"\"The agent refused the job because the signature could not be verified. Note that in this case, no signal was sent to the process, the job was not run at all.\"\"\"\n\tSIGNATURE_REJECTED\n}\n\n\"\"\"An event created when the job is timed out\"\"\"\ntype JobEventTimedOut implements JobEvent & Node{\n\"\"\"The actor that caused this event to occur\"\"\"\n\tactor: JobEventActor!\n\texitStatus: JSInt!\n\tid: ID!\n\"\"\"The job that this event belongs to\"\"\"\n\tjob: JobTypeCommand!\n\"\"\"The termination signal which killed the command, if the command was killed\"\"\"\n\tsignal: String\n\"\"\"If the termination signal was sent by the agent, the reason the agent took that action. If this field is null, and the `signal` field is not null, the command was killed by another process or by the operating system.\"\"\"\n\tsignalReason: JobEventSignalReason\n\"\"\"The time when the event occurred\"\"\"\n\ttimestamp: DateTime!\n\"\"\"The type of event\"\"\"\n\ttype: JobEventType!\n\"\"\"The public UUID for this job event\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"All the possible types of events that happen to a Job\"\"\"\nenum JobEventType {\n\"\"\"The Job was assigned to an agent\"\"\"\n\tASSIGNED\n\"\"\"The agent took too long to accept the job\"\"\"\n\tASSIGNED_EXPIRED\n\"\"\"The Job was accepted by an agent\"\"\"\n\tACCEPTED\n\"\"\"The agent took too long to start the job\"\"\"\n\tACCEPTED_EXPIRED\n\"\"\"The Job was started by an agent\"\"\"\n\tSTARTED\n\"\"\"The Job was finished by an agent\"\"\"\n\tFINISHED\n\"\"\"The Job was canceled\"\"\"\n\tCANCELED\n\"\"\"The Job was timed out\"\"\"\n\tTIMED_OUT\n\"\"\"The Job was retried either automatically or by a user\"\"\"\n\tRETRIED\n\"\"\"The Job was changed\"\"\"\n\tCHANGED\n\"\"\"The Job was unblocked by a user\"\"\"\n\tUNBLOCKED\n\"\"\"The Job was scheduled\"\"\"\n\tSCHEDULED\n\"\"\"The Job sent a notification\"\"\"\n\tNOTIFICATION\n\"\"\"The Job was marked for cancelation by a user\"\"\"\n\tCANCELATION\n\"\"\"The Job is limited by a concurrency group\"\"\"\n\tLIMITED\n\"\"\"The Job uploaded steps to the current build\"\"\"\n\tBUILD_STEP_UPLOAD_CREATED\n\"\"\"The Job expired before it was started on an agent\"\"\"\n\tEXPIRED\n\"\"\"The agent was stopped while processing this job\"\"\"\n\tAGENT_STOPPED\n\"\"\"The agent disconnected while processing this job\"\"\"\n\tAGENT_DISCONNECTED\n\"\"\"The agent was lost while processing this job\"\"\"\n\tAGENT_LOST\n\"\"\"The job log exceeded the limit\"\"\"\n\tLOG_SIZE_LIMIT_EXCEEDED\n}\n\ninterface JobInterface {\n\tretried: Boolean!\n\tretriedBy: User\n\tretriesCount: Int\n\tretrySource: Job\n\tretryType: JobRetryTypes\n\tuuid: String!\n}\n\n\"\"\"A record of job minutes usage, aggregated by day and pipeline.\"\"\"\ntype JobMinutesUsage implements ResourceUsageInterface{\n\taggregatedOn: ISO8601Date!\n\tpipeline: Pipeline\n\tpipelineId: ID!\n\"\"\"The recorded usage in seconds. For billing purposes, seconds are summed for a billing period and rounded down to the nearest minute.\"\"\"\n\tseconds: Int!\n}\n\n\"\"\"The different orders you can sort jobs by\"\"\"\nenum JobOrder {\n\"\"\"Order by the most recently assigned jobs first\"\"\"\n\tRECENTLY_ASSIGNED\n\"\"\"Order by the most recently created jobs first\"\"\"\n\tRECENTLY_CREATED\n}\n\n\"\"\"The priority with which a job will run\"\"\"\ntype JobPriority {\n\tnumber: Int\n}\n\n\"\"\"Search jobs by priority\"\"\"\ninput JobPrioritySearch {\n\"\"\"Search jobs by priority\"\"\"\n\tnumber: [Int!]\n}\n\n\"\"\"Automatic retry rule configuration\"\"\"\ntype JobRetryRuleAutomatic {\n\texitStatus: String\n\tlimit: String\n\tsignal: String\n\tsignalReason: String\n}\n\n\"\"\"Retry Rules for a job\"\"\"\ntype JobRetryRules {\n\tautomatic: [JobRetryRuleAutomatic]\n\tmanual: Boolean\n}\n\n\"\"\"The retry types that can be made on a Job\"\"\"\nenum JobRetryTypes {\n\tMANUAL\n\tAUTOMATIC\n}\n\n\"\"\"All the possible states a job can be in\"\"\"\nenum JobStates {\n\"\"\"The job has just been created and doesn't have a state yet\"\"\"\n\tPENDING\n\"\"\"The job is waiting on a `wait` step to finish\"\"\"\n\tWAITING\n\"\"\"The job was in a `WAITING` state when the build failed\"\"\"\n\tWAITING_FAILED\n\"\"\"The job is waiting on a `block` step to finish\"\"\"\n\tBLOCKED\n\"\"\"The job was in a `BLOCKED` state when the build failed\"\"\"\n\tBLOCKED_FAILED\n\"\"\"This `block` job has been manually unblocked\"\"\"\n\tUNBLOCKED\n\"\"\"This `block` job was in an `UNBLOCKED` state when the build failed\"\"\"\n\tUNBLOCKED_FAILED\n\"\"\"The job is waiting on a concurrency group check before becoming either `LIMITED` or `SCHEDULED`\"\"\"\n\tLIMITING\n\"\"\"The job is waiting for jobs with the same concurrency group to finish\"\"\"\n\tLIMITED\n\"\"\"The job is scheduled and waiting for an agent\"\"\"\n\tSCHEDULED\n\"\"\"The job has been assigned to an agent, and it's waiting for it to accept\"\"\"\n\tASSIGNED\n\"\"\"The job was accepted by the agent, and now it's waiting to start running\"\"\"\n\tACCEPTED\n\"\"\"The job is running\"\"\"\n\tRUNNING\n\"\"\"The job has finished\"\"\"\n\tFINISHED\n\"\"\"The job is currently canceling\"\"\"\n\tCANCELING\n\"\"\"The job was canceled\"\"\"\n\tCANCELED\n\"\"\"The job is timing out for taking too long\"\"\"\n\tTIMING_OUT\n\"\"\"The job timed out\"\"\"\n\tTIMED_OUT\n\"\"\"The job was skipped\"\"\"\n\tSKIPPED\n\"\"\"The jobs configuration means that it can't be run\"\"\"\n\tBROKEN\n\"\"\"The job expired before it was started on an agent\"\"\"\n\tEXPIRED\n}\n\n\"\"\"Searching for jobs based on step information\"\"\"\ninput JobStepSearch {\n\"\"\"Searching for jobs based on step information\"\"\"\n\tkey: [String!]\n}\n\n\"\"\"A type of job that requires a user to unblock it before proceeding in a build pipeline\"\"\"\ntype JobTypeBlock implements JobInterface & Node{\n\"\"\"The build that this job is a part of\"\"\"\n\tbuild: Build\n\tid: ID!\n\"\"\"Whether or not this job can be unblocked yet (may be waiting on another job to finish)\"\"\"\n\tisUnblockable: Boolean\n\"\"\"The label of this block step\"\"\"\n\tlabel: String\n\"\"\"If this job has been retried\"\"\"\n\tretried: Boolean!\n\"\"\"The user that retried this job\"\"\"\n\tretriedBy: User\n\"\"\"The number of times the job has been retried\"\"\"\n\tretriesCount: Int\n\"\"\"The job that was retried to create this job\"\"\"\n\tretrySource: Job\n\"\"\"The type of retry that was performed on this job\"\"\"\n\tretryType: JobRetryTypes\n\"\"\"The state of the job\"\"\"\n\tstate: JobStates!\n\"\"\"The step that defined this job. Some older jobs in the system may not have an associated step\"\"\"\n\tstep: StepInput\n\"\"\"The time when the job was created\"\"\"\n\tunblockedAt: DateTime\n\"\"\"The user that unblocked this job\"\"\"\n\tunblockedBy: User\n\"\"\"The UUID for this job\"\"\"\n\tuuid: String!\n}\n\n\"\"\"Autogenerated input type of JobTypeBlockUnblock\"\"\"\ninput JobTypeBlockUnblockInput {\n\"\"\"Autogenerated input type of JobTypeBlockUnblock\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of JobTypeBlockUnblock\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of JobTypeBlockUnblock\"\"\"\n\tfields: JSON\n}\n\n\"\"\"Autogenerated return type of JobTypeBlockUnblock.\"\"\"\ntype JobTypeBlockUnblockPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tjobTypeBlock: JobTypeBlock!\n}\n\n\"\"\"A type of job that runs a command on an agent\"\"\"\ntype JobTypeCommand implements JobInterface & Node{\n\"\"\"The agent that is running the job\"\"\"\n\tagent: Agent\n\"\"\"The ruleset used to find an agent to run this job\"\"\"\n\tagentQueryRules: [String!]\n\"\"\"Artifacts uploaded to this job\"\"\"\n\tartifacts(\n\t\tfirst: Int\n\t\tlast: Int\n\t): ArtifactConnection\n\"\"\"A glob of files to automatically upload after the job finishes\"\"\"\n\tautomaticArtifactUploadPaths: String\n\"\"\"The build that this job is a part of\"\"\"\n\tbuild: Build\n\"\"\"The time when the job was cancelled\"\"\"\n\tcanceledAt: DateTime\n\"\"\"The cluster of this job\"\"\"\n\tcluster: Cluster\n\"\"\"The cluster queue of this job\"\"\"\n\tclusterQueue: ClusterQueue\n\"\"\"The command the job will run\"\"\"\n\tcommand: String\n\"\"\"Concurrency information related to a job\"\"\"\n\tconcurrency: JobConcurrency\n\"\"\"The time when the job was created\"\"\"\n\tcreatedAt: DateTime\n\"\"\"Environment variables for this job\"\"\"\n\tenv: [String!]\n\"\"\"Job events\"\"\"\n\tevents(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t): JobEventConnection!\n\"\"\"The exit status returned by the command on the agent\"\"\"\n\texitStatus: String\n\"\"\"The time when the job was expired\"\"\"\n\texpiredAt: DateTime\n\"\"\"The time when the job finished\"\"\"\n\tfinishedAt: DateTime\n\tid: ID!\n\"\"\"The label of the job\"\"\"\n\tlabel: String\n\"\"\"The matrix configuration values for this particular job\"\"\"\n\tmatrix: JSON\n\"\"\"The index of this job within the parallel job group it is a part of. Null if this job is not part of a parallel job group.\"\"\"\n\tparallelGroupIndex: Int\n\"\"\"The total number of jobs in the parallel job group this job is a part of. Null if this job is not part of a parallel job group.\"\"\"\n\tparallelGroupTotal: Int\n\"\"\"If the job has finished and passed\"\"\"\n\tpassed: Boolean!\n\"\"\"The pipeline that this job is a part of\"\"\"\n\tpipeline: Pipeline\n\"\"\"The priority of this job\"\"\"\n\tpriority: JobPriority!\n\"\"\"If this job has been retried\"\"\"\n\tretried: Boolean!\n\"\"\"The user that retried this job\"\"\"\n\tretriedBy: User\n\"\"\"The number of times the job has been retried\"\"\"\n\tretriesCount: Int\n\"\"\"Job retry rules\"\"\"\n\tretryRules: JobRetryRules\n\"\"\"The job that was retried to create this job\"\"\"\n\tretrySource: Job\n\"\"\"The type of retry that was performed on this job\"\"\"\n\tretryType: JobRetryTypes\n\"\"\"The time when the job became available to be run by an agent\"\"\"\n\trunnableAt: DateTime\n\"\"\"The time when the job became scheduled for running\"\"\"\n\tscheduledAt: DateTime\n\"\"\"The termination signal which killed the command, if the command was killed\"\"\"\n\tsignal: String\n\"\"\"If the termination signal was sent by the agent, the reason the agent took that action. If this field is null, and the `signal` field is not null, the command was killed by another process or by the operating system.\"\"\"\n\tsignalReason: JobEventSignalReason\n\"\"\"If the job soft failed\"\"\"\n\tsoftFailed: Boolean!\n\"\"\"The time when the job started running\"\"\"\n\tstartedAt: DateTime\n\"\"\"The state of the job\"\"\"\n\tstate: JobStates!\n\"\"\"The step that defined this job. Some older jobs in the system may not have an associated step\"\"\"\n\tstep: StepCommand\n\"\"\"The URL for the job\"\"\"\n\turl: String!\n\"\"\"The UUID for this job\"\"\"\n\tuuid: String!\n}\n\n\"\"\"Autogenerated input type of JobTypeCommandCancel\"\"\"\ninput JobTypeCommandCancelInput {\n\"\"\"Autogenerated input type of JobTypeCommandCancel\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of JobTypeCommandCancel\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of JobTypeCommandCancel.\"\"\"\ntype JobTypeCommandCancelPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tjobTypeCommand: JobTypeCommand!\n}\n\n\"\"\"Autogenerated input type of JobTypeCommandRetry\"\"\"\ninput JobTypeCommandRetryInput {\n\"\"\"Autogenerated input type of JobTypeCommandRetry\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of JobTypeCommandRetry\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of JobTypeCommandRetry.\"\"\"\ntype JobTypeCommandRetryPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tjobTypeCommand: JobTypeCommand!\n\tretriedInJobTypeCommand: JobTypeCommand!\n}\n\n\"\"\"A type of job that triggers another build on a pipeline\"\"\"\ntype JobTypeTrigger implements JobInterface & Node{\n\"\"\"Whether the triggered build runs asynchronously or not\"\"\"\n\tasync: Boolean!\n\"\"\"The build that this job is a part of\"\"\"\n\tbuild: Build\n\tid: ID!\n\"\"\"The label of this trigger step\"\"\"\n\tlabel: String\n\"\"\"If this job has been retried\"\"\"\n\tretried: Boolean!\n\"\"\"The user that retried this job\"\"\"\n\tretriedBy: User\n\"\"\"The number of times the job has been retried\"\"\"\n\tretriesCount: Int\n\"\"\"The job that was retried to create this job\"\"\"\n\tretrySource: Job\n\"\"\"The type of retry that was performed on this job\"\"\"\n\tretryType: JobRetryTypes\n\"\"\"The state of the job\"\"\"\n\tstate: JobStates!\n\"\"\"The step that defined this job. Some older jobs in the system may not have an associated step\"\"\"\n\tstep: StepTrigger\n\"\"\"The build that this job triggered\"\"\"\n\ttriggered: Build\n\"\"\"The UUID for this job\"\"\"\n\tuuid: String!\n}\n\n\"\"\"A type of job that waits for all previous jobs to pass before proceeding the build pipeline\"\"\"\ntype JobTypeWait implements JobInterface & Node{\n\"\"\"The build that this job is a part of\"\"\"\n\tbuild: Build\n\tid: ID!\n\"\"\"The label of this wait step\"\"\"\n\tlabel: String\n\"\"\"If this job has been retried\"\"\"\n\tretried: Boolean!\n\"\"\"The user that retried this job\"\"\"\n\tretriedBy: User\n\"\"\"The number of times the job has been retried\"\"\"\n\tretriesCount: Int\n\"\"\"The job that was retried to create this job\"\"\"\n\tretrySource: Job\n\"\"\"The type of retry that was performed on this job\"\"\"\n\tretryType: JobRetryTypes\n\"\"\"The state of the job\"\"\"\n\tstate: JobStates!\n\"\"\"The step that defined this job. Some older jobs in the system may not have an associated step\"\"\"\n\tstep: StepWait\n\"\"\"The UUID for this job\"\"\"\n\tuuid: String!\n}\n\n\"\"\"All the possible types of jobs that can exist\"\"\"\nenum JobTypes {\n\"\"\"A job that runs a command on an agent\"\"\"\n\tCOMMAND\n\"\"\"A job that waits for all previous jobs to finish\"\"\"\n\tWAIT\n\"\"\"A job that blocks a pipeline from progressing until it's manually unblocked\"\"\"\n\tBLOCK\n\"\"\"A job that triggers another build on a pipeline\"\"\"\n\tTRIGGER\n}\n\n\"\"\"The root for mutations in this schema\"\"\"\ntype Mutation {\n\"\"\"Instruct an agent to stop accepting new build jobs and shut itself down.\"\"\"\n\tagentStop(\n\"\"\"Parameters for AgentStop\"\"\"\n\t\tinput: AgentStopInput!\n\t): AgentStopPayload\n\"\"\"Create a new unclustered agent token.\"\"\"\n\tagentTokenCreate(\n\"\"\"Parameters for AgentTokenCreate\"\"\"\n\t\tinput: AgentTokenCreateInput!\n\t): AgentTokenCreatePayload\n\"\"\"Revoke an unclustered agent token.\"\"\"\n\tagentTokenRevoke(\n\"\"\"Parameters for AgentTokenRevoke\"\"\"\n\t\tinput: AgentTokenRevokeInput!\n\t): AgentTokenRevokePayload\n\"\"\"Authorize an API Access Token Code generated by an API Application. Please note this mutation is private and cannot be executed externally.\"\"\"\n\tapiAccessTokenCodeAuthorize(\n\"\"\"Parameters for APIAccessTokenCodeAuthorizeMutation\"\"\"\n\t\tinput: APIAccessTokenCodeAuthorizeMutationInput!\n\t): APIAccessTokenCodeAuthorizeMutationPayload\n\"\"\"Annotate a build with information to appear on the build page.\"\"\"\n\tbuildAnnotate(\n\"\"\"Parameters for BuildAnnotate\"\"\"\n\t\tinput: BuildAnnotateInput!\n\t): BuildAnnotatePayload\n\"\"\"Cancel a build.\"\"\"\n\tbuildCancel(\n\"\"\"Parameters for BuildCancel\"\"\"\n\t\tinput: BuildCancelInput!\n\t): BuildCancelPayload\n\"\"\"Create a build.\"\"\"\n\tbuildCreate(\n\"\"\"Parameters for BuildCreate\"\"\"\n\t\tinput: BuildCreateInput!\n\t): BuildCreatePayload\n\"\"\"Rebuild a build.\"\"\"\n\tbuildRebuild(\n\"\"\"Parameters for BuildRebuild\"\"\"\n\t\tinput: BuildRebuildInput!\n\t): BuildRebuildPayload\n\"\"\"Create an agent token for a cluster.\"\"\"\n\tclusterAgentTokenCreate(\n\"\"\"Parameters for ClusterAgentTokenCreate\"\"\"\n\t\tinput: ClusterAgentTokenCreateInput!\n\t): ClusterAgentTokenCreatePayload\n\"\"\"Revokes an agent token for a cluster.\"\"\"\n\tclusterAgentTokenRevoke(\n\"\"\"Parameters for ClusterAgentTokenRevoke\"\"\"\n\t\tinput: ClusterAgentTokenRevokeInput!\n\t): ClusterAgentTokenRevokePayload\n\"\"\"Updates an agent token for a cluster.\"\"\"\n\tclusterAgentTokenUpdate(\n\"\"\"Parameters for ClusterAgentTokenUpdate\"\"\"\n\t\tinput: ClusterAgentTokenUpdateInput!\n\t): ClusterAgentTokenUpdatePayload\n\"\"\"Create a cluster.\"\"\"\n\tclusterCreate(\n\"\"\"Parameters for ClusterCreate\"\"\"\n\t\tinput: ClusterCreateInput!\n\t): ClusterCreatePayload\n\"\"\"Delete a cluster.\"\"\"\n\tclusterDelete(\n\"\"\"Parameters for ClusterDelete\"\"\"\n\t\tinput: ClusterDeleteInput!\n\t): ClusterDeletePayload\n\"\"\"Create a cluster queue.\"\"\"\n\tclusterQueueCreate(\n\"\"\"Parameters for ClusterQueueCreate\"\"\"\n\t\tinput: ClusterQueueCreateInput!\n\t): ClusterQueueCreatePayload\n\"\"\"Delete a cluster queue.\"\"\"\n\tclusterQueueDelete(\n\"\"\"Parameters for ClusterQueueDelete\"\"\"\n\t\tinput: ClusterQueueDeleteInput!\n\t): ClusterQueueDeletePayload\n\"\"\"This will prevent dispatch of jobs to agents on this queue. You can add an optional note describing the reason for pausing.\"\"\"\n\tclusterQueuePauseDispatch(\n\"\"\"Parameters for ClusterQueuePauseDispatch\"\"\"\n\t\tinput: ClusterQueuePauseDispatchInput!\n\t): ClusterQueuePauseDispatchPayload\n\"\"\"This will resume dispatch of jobs on this queue.\"\"\"\n\tclusterQueueResumeDispatch(\n\"\"\"Parameters for ClusterQueueResumeDispatch\"\"\"\n\t\tinput: ClusterQueueResumeDispatchInput!\n\t): ClusterQueueResumeDispatchPayload\n\"\"\"Updates a cluster queue.\"\"\"\n\tclusterQueueUpdate(\n\"\"\"Parameters for ClusterQueueUpdate\"\"\"\n\t\tinput: ClusterQueueUpdateInput!\n\t): ClusterQueueUpdatePayload\n\"\"\"Updates a cluster.\"\"\"\n\tclusterUpdate(\n\"\"\"Parameters for ClusterUpdate\"\"\"\n\t\tinput: ClusterUpdateInput!\n\t): ClusterUpdatePayload\n\"\"\"Add a new email address for the current user\"\"\"\n\temailCreate(\n\"\"\"Parameters for EmailCreate\"\"\"\n\t\tinput: EmailCreateInput!\n\t): EmailCreatePayload\n\"\"\"Resend a verification email.\"\"\"\n\temailResendVerification(\n\"\"\"Parameters for EmailResendVerification\"\"\"\n\t\tinput: EmailResendVerificationInput!\n\t): EmailResendVerificationPayload\n\"\"\"Create a GraphQL snippet.\"\"\"\n\tgraphQLSnippetCreate(\n\"\"\"Parameters for GraphQLSnippetCreate\"\"\"\n\t\tinput: GraphQLSnippetCreateInput!\n\t): GraphQLSnippetCreatePayload\n\"\"\"Unblocks a build's \"Block pipeline\" job.\"\"\"\n\tjobTypeBlockUnblock(\n\"\"\"Parameters for JobTypeBlockUnblock\"\"\"\n\t\tinput: JobTypeBlockUnblockInput!\n\t): JobTypeBlockUnblockPayload\n\"\"\"Cancel a job.\"\"\"\n\tjobTypeCommandCancel(\n\"\"\"Parameters for JobTypeCommandCancel\"\"\"\n\t\tinput: JobTypeCommandCancelInput!\n\t): JobTypeCommandCancelPayload\n\"\"\"Retry a job.\"\"\"\n\tjobTypeCommandRetry(\n\"\"\"Parameters for JobTypeCommandRetry\"\"\"\n\t\tinput: JobTypeCommandRetryInput!\n\t): JobTypeCommandRetryPayload\n\"\"\"Dismisses a notice from the Buildkite UI. This mutation is idempotent so if you dismiss the same notice multiple times, it will return the original `dismissedAt` time\"\"\"\n\tnoticeDismiss(\n\"\"\"Parameters for NoticeDismiss\"\"\"\n\t\tinput: NoticeDismissInput!\n\t): NoticeDismissPayload\n\"\"\"Revokes access to an organization for a user's API access token. The organization can not be re-added to the same token, however the user can create a new token and add the organization to that token.\"\"\"\n\torganizationApiAccessTokenRevoke(\n\"\"\"Parameters for OrganizationAPIAccessTokenRevokeMutation\"\"\"\n\t\tinput: OrganizationAPIAccessTokenRevokeMutationInput!\n\t): OrganizationAPIAccessTokenRevokeMutationPayload\n\"\"\"Sets an allowlist of IP addresses for API access to an organization. Please note that this is a beta feature and is not yet available to all organizations. \"\"\"\n\torganizationApiIpAllowlistUpdate(\n\"\"\"Parameters for OrganizationAPIIPAllowlistUpdateMutation\"\"\"\n\t\tinput: OrganizationAPIIPAllowlistUpdateMutationInput!\n\t): OrganizationAPIIPAllowlistUpdateMutationPayload\n\"\"\"Delete the system banner\"\"\"\n\torganizationBannerDelete(\n\"\"\"Parameters for OrganizationBannerDelete\"\"\"\n\t\tinput: OrganizationBannerDeleteInput!\n\t): OrganizationBannerDeletePayload\n\"\"\"Retrieves the active system banner for provided organization, then updates it with input data. If active banner is not found, a new banner is created with the provided input.\"\"\"\n\torganizationBannerUpsert(\n\"\"\"Parameters for OrganizationBannerUpsert\"\"\"\n\t\tinput: OrganizationBannerUpsertInput!\n\t): OrganizationBannerUpsertPayload\n\"\"\"Sets whether the organization requires two-factor authentication for all members.\"\"\"\n\torganizationEnforceTwoFactorAuthenticationForMembersUpdate(\n\"\"\"Parameters for OrganizationEnforceTwoFactorAuthenticationForMembersUpdateMutation\"\"\"\n\t\tinput: OrganizationEnforceTwoFactorAuthenticationForMembersUpdateMutationInput!\n\t): OrganizationEnforceTwoFactorAuthenticationForMembersUpdateMutationPayload\n\"\"\"Send email invitations to this organization.\"\"\"\n\torganizationInvitationCreate(\n\"\"\"Parameters for OrganizationInvitationCreate\"\"\"\n\t\tinput: OrganizationInvitationCreateInput!\n\t): OrganizationInvitationCreatePayload\n\"\"\"Resend an organization invitation email.\"\"\"\n\torganizationInvitationResend(\n\"\"\"Parameters for OrganizationInvitationResend\"\"\"\n\t\tinput: OrganizationInvitationResendInput!\n\t): OrganizationInvitationResendPayload\n\"\"\"Revoke an invitation to an organization so that it can no longer be accepted.\"\"\"\n\torganizationInvitationRevoke(\n\"\"\"Parameters for OrganizationInvitationRevoke\"\"\"\n\t\tinput: OrganizationInvitationRevokeInput!\n\t): OrganizationInvitationRevokePayload\n\"\"\"Remove a user from an organization.\"\"\"\n\torganizationMemberDelete(\n\"\"\"Parameters for OrganizationMemberDelete\"\"\"\n\t\tinput: OrganizationMemberDeleteInput!\n\t): OrganizationMemberDeletePayload\n\"\"\"Change a user's role within an organization.\"\"\"\n\torganizationMemberUpdate(\n\"\"\"Parameters for OrganizationMemberUpdate\"\"\"\n\t\tinput: OrganizationMemberUpdateInput!\n\t): OrganizationMemberUpdatePayload\n\"\"\"Specify the maximum timeframe to revoke organization access from inactive API tokens.\"\"\"\n\torganizationRevokeInactiveTokensAfterUpdate(\n\"\"\"Parameters for OrganizationRevokeInactiveTokensAfterUpdateMutation\"\"\"\n\t\tinput: OrganizationRevokeInactiveTokensAfterUpdateMutationInput!\n\t): OrganizationRevokeInactiveTokensAfterUpdateMutationPayload\n\"\"\"Archive a pipeline.\"\"\"\n\tpipelineArchive(\n\"\"\"Parameters for PipelineArchive\"\"\"\n\t\tinput: PipelineArchiveInput!\n\t): PipelineArchivePayload\n\"\"\"Create a pipeline.\"\"\"\n\tpipelineCreate(\n\"\"\"Parameters for PipelineCreate\"\"\"\n\t\tinput: PipelineCreateInput!\n\t): PipelineCreatePayload\n\"\"\"Create SCM webhooks for a pipeline.\"\"\"\n\tpipelineCreateWebhook(\n\"\"\"Parameters for PipelineCreateWebhook\"\"\"\n\t\tinput: PipelineCreateWebhookInput!\n\t): PipelineCreateWebhookPayload\n\"\"\"Delete a pipeline.\"\"\"\n\tpipelineDelete(\n\"\"\"Parameters for PipelineDelete\"\"\"\n\t\tinput: PipelineDeleteInput!\n\t): PipelineDeletePayload\n\"\"\"Favorite a pipeline.\"\"\"\n\tpipelineFavorite(\n\"\"\"Parameters for PipelineFavorite\"\"\"\n\t\tinput: PipelineFavoriteInput!\n\t): PipelineFavoritePayload\n\"\"\"Rotate a pipeline's webhook URL.\n\nNote that the old webhook URL will stop working immediately and so must be updated quickly to avoid interruption.\n\"\"\"\n\tpipelineRotateWebhookURL(\n\"\"\"Parameters for PipelineRotateWebhookURL\"\"\"\n\t\tinput: PipelineRotateWebhookURLInput!\n\t): PipelineRotateWebhookURLPayload\n\"\"\"Create a scheduled build on pipeline.\"\"\"\n\tpipelineScheduleCreate(\n\"\"\"Parameters for PipelineScheduleCreate\"\"\"\n\t\tinput: PipelineScheduleCreateInput!\n\t): PipelineScheduleCreatePayload\n\"\"\"Delete a scheduled build on pipeline.\"\"\"\n\tpipelineScheduleDelete(\n\"\"\"Parameters for PipelineScheduleDelete\"\"\"\n\t\tinput: PipelineScheduleDeleteInput!\n\t): PipelineScheduleDeletePayload\n\"\"\"Update a scheduled build on pipeline.\"\"\"\n\tpipelineScheduleUpdate(\n\"\"\"Parameters for PipelineScheduleUpdate\"\"\"\n\t\tinput: PipelineScheduleUpdateInput!\n\t): PipelineScheduleUpdatePayload\n\"\"\"Create a pipeline template.\"\"\"\n\tpipelineTemplateCreate(\n\"\"\"Parameters for PipelineTemplateCreate\"\"\"\n\t\tinput: PipelineTemplateCreateInput!\n\t): PipelineTemplateCreatePayload\n\"\"\"Delete a pipeline template.\"\"\"\n\tpipelineTemplateDelete(\n\"\"\"Parameters for PipelineTemplateDelete\"\"\"\n\t\tinput: PipelineTemplateDeleteInput!\n\t): PipelineTemplateDeletePayload\n\"\"\"Update a pipeline template.\"\"\"\n\tpipelineTemplateUpdate(\n\"\"\"Parameters for PipelineTemplateUpdate\"\"\"\n\t\tinput: PipelineTemplateUpdateInput!\n\t): PipelineTemplateUpdatePayload\n\"\"\"Unarchive a pipeline.\"\"\"\n\tpipelineUnarchive(\n\"\"\"Parameters for PipelineUnarchive\"\"\"\n\t\tinput: PipelineUnarchiveInput!\n\t): PipelineUnarchivePayload\n\"\"\"Change the settings for a pipeline.\"\"\"\n\tpipelineUpdate(\n\"\"\"Parameters for PipelineUpdate\"\"\"\n\t\tinput: PipelineUpdateInput!\n\t): PipelineUpdatePayload\n\"\"\"Create a SSO provider.\"\"\"\n\tssoProviderCreate(\n\"\"\"Parameters for SSOProviderCreate\"\"\"\n\t\tinput: SSOProviderCreateInput!\n\t): SSOProviderCreatePayload\n\"\"\"Delete a SSO provider.\"\"\"\n\tssoProviderDelete(\n\"\"\"Parameters for SSOProviderDelete\"\"\"\n\t\tinput: SSOProviderDeleteInput!\n\t): SSOProviderDeletePayload\n\"\"\"Disable a SSO provider.\"\"\"\n\tssoProviderDisable(\n\"\"\"Parameters for SSOProviderDisable\"\"\"\n\t\tinput: SSOProviderDisableInput!\n\t): SSOProviderDisablePayload\n\"\"\"Enable a SSO provider.\"\"\"\n\tssoProviderEnable(\n\"\"\"Parameters for SSOProviderEnable\"\"\"\n\t\tinput: SSOProviderEnableInput!\n\t): SSOProviderEnablePayload\n\"\"\"Change the settings for a SSO provider.\"\"\"\n\tssoProviderUpdate(\n\"\"\"Parameters for SSOProviderUpdate\"\"\"\n\t\tinput: SSOProviderUpdateInput!\n\t): SSOProviderUpdatePayload\n\"\"\"Create a team.\"\"\"\n\tteamCreate(\n\"\"\"Parameters for TeamCreate\"\"\"\n\t\tinput: TeamCreateInput!\n\t): TeamCreatePayload\n\"\"\"Delete a team.\"\"\"\n\tteamDelete(\n\"\"\"Parameters for TeamDelete\"\"\"\n\t\tinput: TeamDeleteInput!\n\t): TeamDeletePayload\n\"\"\"Add a user to a team.\"\"\"\n\tteamMemberCreate(\n\"\"\"Parameters for TeamMemberCreate\"\"\"\n\t\tinput: TeamMemberCreateInput!\n\t): TeamMemberCreatePayload\n\"\"\"Remove a user from a team.\"\"\"\n\tteamMemberDelete(\n\"\"\"Parameters for TeamMemberDelete\"\"\"\n\t\tinput: TeamMemberDeleteInput!\n\t): TeamMemberDeletePayload\n\"\"\"Update a user's role in a team.\"\"\"\n\tteamMemberUpdate(\n\"\"\"Parameters for TeamMemberUpdate\"\"\"\n\t\tinput: TeamMemberUpdateInput!\n\t): TeamMemberUpdatePayload\n\"\"\"Add a pipeline to a team.\"\"\"\n\tteamPipelineCreate(\n\"\"\"Parameters for TeamPipelineCreate\"\"\"\n\t\tinput: TeamPipelineCreateInput!\n\t): TeamPipelineCreatePayload\n\"\"\"Remove a pipeline from a team.\"\"\"\n\tteamPipelineDelete(\n\"\"\"Parameters for TeamPipelineDelete\"\"\"\n\t\tinput: TeamPipelineDeleteInput!\n\t): TeamPipelineDeletePayload\n\"\"\"Update a pipeline's access level within a team.\"\"\"\n\tteamPipelineUpdate(\n\"\"\"Parameters for TeamPipelineUpdate\"\"\"\n\t\tinput: TeamPipelineUpdateInput!\n\t): TeamPipelineUpdatePayload\n\"\"\"Add a suite to a team.\"\"\"\n\tteamSuiteCreate(\n\"\"\"Parameters for TeamSuiteCreate\"\"\"\n\t\tinput: TeamSuiteCreateInput!\n\t): TeamSuiteCreatePayload\n\"\"\"Remove a suite from a team.\"\"\"\n\tteamSuiteDelete(\n\"\"\"Parameters for TeamSuiteDelete\"\"\"\n\t\tinput: TeamSuiteDeleteInput!\n\t): TeamSuiteDeletePayload\n\"\"\"Update a suite's access level within a team.\"\"\"\n\tteamSuiteUpdate(\n\"\"\"Parameters for TeamSuiteUpdate\"\"\"\n\t\tinput: TeamSuiteUpdateInput!\n\t): TeamSuiteUpdatePayload\n\"\"\"Change the settings for a team.\"\"\"\n\tteamUpdate(\n\"\"\"Parameters for TeamUpdate\"\"\"\n\t\tinput: TeamUpdateInput!\n\t): TeamUpdatePayload\n\"\"\"Activate a previously-generated TOTP configuration, and its Recovery Codes.\n\nOnce activated, both this TOTP configuration, and the associated Recovery Codes will become active for the user.\nAny previous TOTP configuration or Recovery Codes will no longer be usable.\n\nThis mutation is private, requires an escalated session, and cannot be accessed via the public GraphQL API.\n\"\"\"\n\ttotpActivate(\n\"\"\"Parameters for TOTPActivate\"\"\"\n\t\tinput: TOTPActivateInput!\n\t): TOTPActivatePayload\n\"\"\"Create a new TOTP configuration for the current user.\n\nThis will produce a TOTP configuration with an associated set of Recovery Codes. The Recovery Codes must be presented to the user prior to the TOTP's activation with `totpActivate`.\nNeither TOTP configuration nor Recovery Codes will be usable until they have been activated.\n\nThis mutation is private, requires an escalated session, and cannot be accessed via the public GraphQL API.\n\"\"\"\n\ttotpCreate(\n\"\"\"Parameters for TOTPCreate\"\"\"\n\t\tinput: TOTPCreateInput!\n\t): TOTPCreatePayload\n\"\"\"Delete a TOTP configuration.\n\nIf a TOTP configuration was active, it will no longer be used for logging on to the user's account.\nAny Recovery Codes associated with the TOTP configuration will also no longer be usable.\n\nThis mutation is private, requires an escalated session, and cannot be accessed via the public GraphQL API.\n\"\"\"\n\ttotpDelete(\n\"\"\"Parameters for TOTPDelete\"\"\"\n\t\tinput: TOTPDeleteInput!\n\t): TOTPDeletePayload\n\"\"\"Generate a new set of Recovery Codes for a given TOTP.\n\nThe new Recovery Codes will immediately replace any existing recovery codes.\n\nThis mutation is private, requires an escalated session, and cannot be accessed via the public GraphQL API.\n\"\"\"\n\ttotpRecoveryCodesRegenerate(\n\"\"\"Parameters for TOTPRecoveryCodesRegenerate\"\"\"\n\t\tinput: TOTPRecoveryCodesRegenerateInput!\n\t): TOTPRecoveryCodesRegeneratePayload\n}\n\n\"\"\"An object with an ID.\"\"\"\ninterface Node {\n\"\"\"An object with an ID.\"\"\"\n\tid: ID!\n}\n\n\"\"\"A notice or notice that a user sees in the Buildkite UI\"\"\"\ntype Notice {\n\"\"\"The time when this notice was dismissed from the UI\"\"\"\n\tdismissedAt: DateTime\n\tid: ID!\n\"\"\"The namespace of this notice\"\"\"\n\tnamespace: NoticeNamespaces!\n\"\"\"The scope within the namespace\"\"\"\n\tscope: String!\n}\n\n\"\"\"Autogenerated input type of NoticeDismiss\"\"\"\ninput NoticeDismissInput {\n\"\"\"Autogenerated input type of NoticeDismiss\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of NoticeDismiss\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of NoticeDismiss.\"\"\"\ntype NoticeDismissPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tnotice: Notice\n}\n\n\"\"\"All the possible namespaces for a notice\"\"\"\nenum NoticeNamespaces {\n\"\"\"A change to an existing feature\"\"\"\n\tCHANGE\n\"\"\"The user has had an email suggested to them\"\"\"\n\tEMAIL_SUGGESTION\n\"\"\"A new feature was added\"\"\"\n\tFEATURE\n\"\"\"An event announcement\"\"\"\n\tEVENT\n}\n\ninterface NotificationService {\n\tdescription: String!\n\tid: ID!\n\tname: String!\n}\n\n\"\"\"Deliver notifications to Slack\"\"\"\ntype NotificationServiceSlack implements Node & NotificationService{\n\"\"\"The description of this service\"\"\"\n\tdescription: String!\n\tid: ID!\n\"\"\"The name of the service provider\"\"\"\n\tname: String!\n}\n\n\"\"\"Deliver notifications to a custom URL\"\"\"\ntype NotificationServiceWebhook implements NotificationService{\n\"\"\"The description of this service\"\"\"\n\tdescription: String!\n\tid: ID!\n\"\"\"The name of the service provider\"\"\"\n\tname: String!\n}\n\n\"\"\"A operating system that an agent can run on\"\"\"\ntype OperatingSystem {\n\"\"\"The name of the operating system\"\"\"\n\tname: String!\n}\n\n\"\"\"An organization\"\"\"\ntype Organization implements Node{\n\"\"\"Returns agent access tokens for an Organization. By default returns all tokens, whether revoked or non-revoked.\"\"\"\n\tagentTokens(\n\t\tfirst: Int\n\t\tlast: Int\n\"\"\"Filter tokens by whether they are revoked or not\"\"\"\n\t\trevoked: Boolean\n\t): AgentTokenConnection\n\tagents(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\"\"\"Search agents for the given query terms case insensitively across name and meta data\"\"\"\n\t\tsearch: String\n\"\"\"Filter agents to those only having the matching meta data\"\"\"\n\t\tmetaData: [String!]\n\"\"\"Filter agents by membership of a given cluster\"\"\"\n\t\tcluster: ID\n\"\"\"Filter agents to those within a given cluster queue\"\"\"\n\t\tclusterQueue: [ID!]\n\"\"\"Pass `false` to exclude agents that belong to a cluster queue\"\"\"\n\t\tclustered: Boolean\n\"\"\"Filter agents by whether they are running a job or not\"\"\"\n\t\tisRunningJob: Boolean\n\t): AgentConnection\n\"\"\"A space-separated allowlist of IP addresses that can access the organization via the GraphQL or REST API\"\"\"\n\tallowedApiIpAddresses: String\n\"\"\"Returns user API access tokens that can access this organization\"\"\"\n\tapiAccessTokens(\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\t): OrganizationAPIAccessTokenConnection!\n\tauditEvents(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\"\"\"Filter events which occurred from the given date and time\"\"\"\n\t\toccurredAtFrom: DateTime\n\"\"\"Filter events which occurred until the given date and time\"\"\"\n\t\toccurredAtTo: DateTime\n\"\"\"Filter the events by type\"\"\"\n\t\ttype: [AuditEventType!]\n\"\"\"Filter the events by the type of actor who initiated them\"\"\"\n\t\tactorType: [AuditActorType!]\n\"\"\"Filter the events by the IDs of the actors who initiated them\"\"\"\n\t\tactor: [ID!]\n\"\"\"Filter the events by the type of subject they relate to\"\"\"\n\t\tsubjectType: [AuditSubjectType!]\n\"\"\"Filter the events by the IDs of the subject they relate to\"\"\"\n\t\tsubject: [ID!]\n\"\"\"Order the events\"\"\"\n\t\torder: OrganizationAuditEventOrders\n\"\"\"Filter the events by the UUIDs of the subject they relate to\"\"\"\n\t\tsubjectUUID: [ID!]\n\t): OrganizationAuditEventConnection\n\"\"\"Returns active banners for this organization.\"\"\"\n\tbanners(\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\t): OrganizationBannerConnection!\n\"\"\"Return cluster in the Organization by UUID\"\"\"\n\tcluster(\n\t\tid: ID!\n\t): Cluster\n\"\"\"Returns clusters for an Organization\"\"\"\n\tclusters(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\"\"\"Order the clusters\"\"\"\n\t\torder: ClusterOrder\n\t): ClusterConnection\n\"\"\"The URL to an icon representing this organization\"\"\"\n\ticonUrl: String\n\tid: ID!\n\tinvitations(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\t\tstate: [OrganizationInvitationStates!]\n\"\"\"Order the invitations\"\"\"\n\t\torder: OrganizationInvitationOrders\n\t): OrganizationInvitationConnection\n\"\"\"Whether teams is enabled for this organization\"\"\"\n\tisTeamsEnabled: Boolean!\n\tjobs(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t\ttype: [JobTypes!]\n\t\tstate: [JobStates!]\n\t\tpriority: JobPrioritySearch\n\t\tagentQueryRules: [String!]\n\t\tconcurrency: JobConcurrencySearch\n\"\"\"Whether or not the command job passed. Passing `false` will return all failed jobs (including \"soft failed\" jobs)\"\"\"\n\t\tpassed: Boolean\n\"\"\"Filtering jobs based on related step information\"\"\"\n\t\tstep: JobStepSearch\n\"\"\"Order the jobs\"\"\"\n\t\torder: JobOrder\n\"\"\"Filter jobs by membership of a given cluster\"\"\"\n\t\tcluster: ID\n\"\"\"Filter jobs to those within a given cluster queue\"\"\"\n\t\tclusterQueue: [ID!]\n\"\"\"Pass `false` to exclude jobs that belong to a cluster queue\"\"\"\n\t\tclustered: Boolean\n\t): JobConnection\n\"\"\"Returns users within the organization\"\"\"\n\tmembers(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\"\"\"Search members named like the given query case insensitively\"\"\"\n\t\tsearch: String\n\"\"\"The primary email of the team member\"\"\"\n\t\temail: String\n\"\"\"Filter the members by team\"\"\"\n\t\tteam: TeamSelector\n\"\"\"Search members by their role\"\"\"\n\t\trole: [OrganizationMemberRole!]\n\t\tsecurity: OrganizationMemberSecurityInput\n\t\tsso: OrganizationMemberSSOInput\n\"\"\"Order the members\"\"\"\n\t\torder: OrganizationMemberOrder\n\t): OrganizationMemberConnection\n\"\"\"Whether this organization requires 2FA to access (Please note that this is a beta feature and is not yet available to all organizations.)\"\"\"\n\tmembersRequireTwoFactorAuthentication: Boolean!\n\"\"\"The name of the organization\"\"\"\n\tname: String!\n\tpermissions: OrganizationPermissions!\n\"\"\"Return all the pipeline templates the current user has access to for this organization\"\"\"\n\tpipelineTemplates(\n\t\tfirst: Int\n\t\tlast: Int\n\t\tafter: String\n\t\tbefore: String\n\"\"\"Order the pipeline templates\"\"\"\n\t\torder: PipelineTemplateOrder\n\t): PipelineTemplateConnection\n\"\"\"Return all the pipelines the current user has access to for this organization\"\"\"\n\tpipelines(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\"\"\"Search pipelines named like the given query case insensitively\"\"\"\n\t\tsearch: String\n\t\trepository: PipelineRepositoryInput\n\"\"\"Filter pipelines by membership of a given cluster\"\"\"\n\t\tcluster: ID\n\"\"\"Pass `false` to exclude pipelines that belong to a cluster\"\"\"\n\t\tclustered: Boolean\n\"\"\"Filter pipelines based on whether or not they've been archived. If not provided, all pipelines are returned regardless of archived state.\"\"\"\n\t\tarchived: Boolean\n\"\"\"Filter the pipelines by team\"\"\"\n\t\tteam: TeamSelector\n\"\"\"Only return favorited pipelines\"\"\"\n\t\tfavorite: Boolean\n\"\"\"Order the pipelines\"\"\"\n\t\torder: PipelineOrders\n\"\"\"Filter pipelines with those that have particular tags\"\"\"\n\t\ttags: [String!]\n\t\tcreatedAtFrom: DateTime\n\t\tcreatedAtTo: DateTime\n\t): PipelineConnection\n\"\"\"Whether this organization is visible to everyone, including people outside it\"\"\"\n\tpublic: Boolean!\n\"\"\"API tokens with access to this organization will be automatically revoked after this many seconds of inactivity. A `null` value indicates never revoke inactive tokens.\"\"\"\n\trevokeInactiveTokensAfter: RevokeInactiveTokenPeriod\n\"\"\"The slug used to represent the organization in URLs\"\"\"\n\tslug: String!\n\"\"\"The single sign-on configuration of this organization\"\"\"\n\tsso: OrganizationSSO\n\"\"\"Single sign on providers created for an organization\"\"\"\n\tssoProviders(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t): SSOProviderConnection\n\"\"\"Return all the suite the current user has access to for this organization\"\"\"\n\tsuites(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\"\"\"Search suites named like the given query case insensitively\"\"\"\n\t\tsearch: String\n\"\"\"Filter the suites by team\"\"\"\n\t\tteam: TeamSelector\n\"\"\"Order the suites\"\"\"\n\t\torder: SuiteOrders\n\t\tcreatedAtFrom: DateTime\n\t\tcreatedAtTo: DateTime\n\t): SuiteConnection\n\"\"\"Returns teams within the organization that the viewer can see\"\"\"\n\tteams(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\"\"\"Search teams\"\"\"\n\t\tsearch: String\n\"\"\"Filter teams by pipeline\"\"\"\n\t\tpipeline: PipelineSelector\n\"\"\"Filter teams by user membership\"\"\"\n\t\tuser: UserSelector\n\"\"\"Search teams by their privacy\"\"\"\n\t\tprivacy: [TeamPrivacy!]\n\"\"\"Order the teams\"\"\"\n\t\torder: TeamOrder\n\t): TeamConnection\n\"\"\"Returns the resource usage data for this organization.\"\"\"\n\tusage(\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Filter aggregations performed from this date\"\"\"\n\t\taggregatedOnFrom: ISO8601Date\n\"\"\"Filter aggregations performed until this date\"\"\"\n\t\taggregatedOnTo: ISO8601Date\n\"\"\"Filter results by resource type\"\"\"\n\t\tresource: [ResourceUsageType!]\n\"\"\"Filter results by the associated Pipeline ID\"\"\"\n\t\tpipelineIds: [ID!]\n\"\"\"Filter results by the associated Suite ID\"\"\"\n\t\tsuiteIds: [ID!]\n\t): UsageUnionConnection!\n\"\"\"The public UUID for this organization\"\"\"\n\tuuid: String!\n}\n\n\"\"\"Information on user API Access Tokens which can access the Organization. Excludes the token attribute\"\"\"\ntype OrganizationAPIAccessToken {\n\tcreatedAt: DateTime!\n\"\"\"A description of the token\"\"\"\n\tdescription: String\n\tid: ID!\n\"\"\"The IP address of the last request to the Buildkite API\"\"\"\n\tipAddress: String\n\"\"\"The last time the token was used to access the Buildkite API\"\"\"\n\tlastAccessedAt: DateTime\n\"\"\"The user associated with this token\"\"\"\n\towner: User\n\"\"\"The organization scopes that the user's token has access to\"\"\"\n\tscopes: [APIAccessTokenScopes!]!\n\"\"\"The public UUID for the API Access Token\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"The connection type for OrganizationAPIAccessToken.\"\"\"\ntype OrganizationAPIAccessTokenConnection {\n\"\"\"A list of edges.\"\"\"\n\tedges: [OrganizationAPIAccessTokenEdge]\n\"\"\"A list of nodes.\"\"\"\n\tnodes: [OrganizationAPIAccessToken]\n\"\"\"Information to aid in pagination.\"\"\"\n\tpageInfo: PageInfo!\n}\n\n\"\"\"An edge in a connection.\"\"\"\ntype OrganizationAPIAccessTokenEdge {\n\"\"\"A cursor for use in pagination.\"\"\"\n\tcursor: String!\n\"\"\"The item at the end of the edge.\"\"\"\n\tnode: OrganizationAPIAccessToken\n}\n\n\"\"\"Autogenerated input type of OrganizationAPIAccessTokenRevokeMutation\"\"\"\ninput OrganizationAPIAccessTokenRevokeMutationInput {\n\"\"\"Autogenerated input type of OrganizationAPIAccessTokenRevokeMutation\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of OrganizationAPIAccessTokenRevokeMutation\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of OrganizationAPIAccessTokenRevokeMutation\"\"\"\n\tapiAccessTokenId: ID!\n}\n\n\"\"\"Autogenerated return type of OrganizationAPIAccessTokenRevokeMutation.\"\"\"\ntype OrganizationAPIAccessTokenRevokeMutationPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\trevokedApiAccessTokenId: ID!\n}\n\n\"\"\"Autogenerated input type of OrganizationAPIIPAllowlistUpdateMutation\"\"\"\ninput OrganizationAPIIPAllowlistUpdateMutationInput {\n\"\"\"Autogenerated input type of OrganizationAPIIPAllowlistUpdateMutation\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of OrganizationAPIIPAllowlistUpdateMutation\"\"\"\n\torganizationID: ID!\n\"\"\"Autogenerated input type of OrganizationAPIIPAllowlistUpdateMutation\"\"\"\n\tipAddresses: String!\n}\n\n\"\"\"Autogenerated return type of OrganizationAPIIPAllowlistUpdateMutation.\"\"\"\ntype OrganizationAPIIPAllowlistUpdateMutationPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\torganization: Organization\n}\n\ntype OrganizationAuditEventConnection implements Connection{\n\tcount: Int!\n\tedges: [OrganizationAuditEventEdge]\n\tpageInfo: PageInfo\n}\n\ntype OrganizationAuditEventEdge {\n\tcursor: String!\n\tnode: AuditEvent\n}\n\n\"\"\"The different orders you can sort audit events by\"\"\"\nenum OrganizationAuditEventOrders {\n\"\"\"Order by the most recently occurring events first\"\"\"\n\tRECENTLY_OCCURRED\n}\n\n\"\"\"System banner of an organization\"\"\"\ntype OrganizationBanner implements Node{\n\tid: ID!\n\"\"\"The banner message\"\"\"\n\tmessage: String!\n\"\"\"The UUID of the organization banner\"\"\"\n\tuuid: String!\n}\n\n\"\"\"The connection type for OrganizationBanner.\"\"\"\ntype OrganizationBannerConnection {\n\"\"\"A list of edges.\"\"\"\n\tedges: [OrganizationBannerEdge]\n\"\"\"A list of nodes.\"\"\"\n\tnodes: [OrganizationBanner]\n\"\"\"Information to aid in pagination.\"\"\"\n\tpageInfo: PageInfo!\n}\n\n\"\"\"Autogenerated input type of OrganizationBannerDelete\"\"\"\ninput OrganizationBannerDeleteInput {\n\"\"\"Autogenerated input type of OrganizationBannerDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of OrganizationBannerDelete\"\"\"\n\torganizationId: ID!\n}\n\n\"\"\"Autogenerated return type of OrganizationBannerDelete.\"\"\"\ntype OrganizationBannerDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedBannerId: ID!\n}\n\n\"\"\"An edge in a connection.\"\"\"\ntype OrganizationBannerEdge {\n\"\"\"A cursor for use in pagination.\"\"\"\n\tcursor: String!\n\"\"\"The item at the end of the edge.\"\"\"\n\tnode: OrganizationBanner\n}\n\n\"\"\"Autogenerated input type of OrganizationBannerUpsert\"\"\"\ninput OrganizationBannerUpsertInput {\n\"\"\"Autogenerated input type of OrganizationBannerUpsert\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of OrganizationBannerUpsert\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of OrganizationBannerUpsert\"\"\"\n\tmessage: String!\n}\n\n\"\"\"Autogenerated return type of OrganizationBannerUpsert.\"\"\"\ntype OrganizationBannerUpsertPayload {\n\tbanner: OrganizationBanner!\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n}\n\ntype OrganizationConnection implements Connection{\n\tcount: Int!\n\tedges: [OrganizationEdge]\n\tpageInfo: PageInfo\n}\n\ntype OrganizationEdge {\n\tcursor: String!\n\tnode: Organization\n}\n\n\"\"\"Autogenerated input type of OrganizationEnforceTwoFactorAuthenticationForMembersUpdateMutation\"\"\"\ninput OrganizationEnforceTwoFactorAuthenticationForMembersUpdateMutationInput {\n\"\"\"Autogenerated input type of OrganizationEnforceTwoFactorAuthenticationForMembersUpdateMutation\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of OrganizationEnforceTwoFactorAuthenticationForMembersUpdateMutation\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of OrganizationEnforceTwoFactorAuthenticationForMembersUpdateMutation\"\"\"\n\tmembersRequireTwoFactorAuthentication: Boolean!\n}\n\n\"\"\"Autogenerated return type of OrganizationEnforceTwoFactorAuthenticationForMembersUpdateMutation.\"\"\"\ntype OrganizationEnforceTwoFactorAuthenticationForMembersUpdateMutationPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\torganization: Organization!\n}\n\n\"\"\"A pending invitation to a user to join this organization\"\"\"\ntype OrganizationInvitation implements Node{\n\"\"\"The time when the invitation was accepted\"\"\"\n\tacceptedAt: DateTime\n\"\"\"The user that accepted this invite\"\"\"\n\tacceptedBy: User\n\"\"\"The time when the invitation was created\"\"\"\n\tcreatedAt: DateTime\n\"\"\"The user that added invited this email address\"\"\"\n\tcreatedBy: User\n\"\"\"The email address of this invitation\"\"\"\n\temail: String!\n\"\"\"The time when the invitation was automatically expired\"\"\"\n\texpiredAt: DateTime\n\tid: ID!\n\torganization: Organization\n\tpermissions: OrganizationInvitationPermissions!\n\"\"\"The time when this invitation was revoked\"\"\"\n\trevokedAt: DateTime\n\"\"\"The user that revoked this invitation\"\"\"\n\trevokedBy: User\n\"\"\"The role the user will have in the organization once they've accepted the invitation\"\"\"\n\trole: OrganizationMemberRole!\n\"\"\"The slug of the invitation that can be used to find an invitation in the query root\"\"\"\n\tslug: String!\n\tsso: OrganizationInvitationSSOType!\n\"\"\"The current state of the invitation\"\"\"\n\tstate: OrganizationInvitationStates!\n\"\"\"Teams that have been assigned to this invitation\"\"\"\n\tteams(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\t): OrganizationInvitationTeamAssignmentConnection\n\"\"\"The UUID of the invitation\"\"\"\n\tuuid: String!\n}\n\ntype OrganizationInvitationConnection implements Connection{\n\tcount: Int!\n\tedges: [OrganizationInvitationEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of OrganizationInvitationCreate\"\"\"\ninput OrganizationInvitationCreateInput {\n\"\"\"Autogenerated input type of OrganizationInvitationCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of OrganizationInvitationCreate\"\"\"\n\torganizationID: ID!\n\"\"\"Autogenerated input type of OrganizationInvitationCreate\"\"\"\n\temails: [String!]!\n\"\"\"Autogenerated input type of OrganizationInvitationCreate\"\"\"\n\trole: OrganizationMemberRole\n\"\"\"Autogenerated input type of OrganizationInvitationCreate\"\"\"\n\tsso: OrganizationInvitationSSOInput\n\"\"\"Autogenerated input type of OrganizationInvitationCreate\"\"\"\n\tteams: [OrganizationInvitationTeamAssignmentInput!]\n}\n\n\"\"\"Autogenerated return type of OrganizationInvitationCreate.\"\"\"\ntype OrganizationInvitationCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tinvitationEdges: [OrganizationInvitationEdge]\n\torganization: Organization\n}\n\ntype OrganizationInvitationEdge {\n\tcursor: String!\n\tnode: OrganizationInvitation\n}\n\n\"\"\"The different orders you can sort organization invitations by\"\"\"\nenum OrganizationInvitationOrders {\n\"\"\"Order by email address alphabetically\"\"\"\n\tEMAIL\n\"\"\"Order by the most recently created invitations first\"\"\"\n\tRECENTLY_CREATED\n}\n\n\"\"\"Permissions information about what actions the current user can do against this invitation\"\"\"\ntype OrganizationInvitationPermissions {\n\"\"\"Whether the user can resend this invitation\"\"\"\n\torganizationInvitationResend: Permission\n\"\"\"Whether the user can revoke this invitation\"\"\"\n\torganizationInvitationRevoke: Permission\n}\n\n\"\"\"Autogenerated input type of OrganizationInvitationResend\"\"\"\ninput OrganizationInvitationResendInput {\n\"\"\"Autogenerated input type of OrganizationInvitationResend\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of OrganizationInvitationResend\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of OrganizationInvitationResend.\"\"\"\ntype OrganizationInvitationResendPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\torganizationInvitation: OrganizationInvitation!\n}\n\n\"\"\"Autogenerated input type of OrganizationInvitationRevoke\"\"\"\ninput OrganizationInvitationRevokeInput {\n\"\"\"Autogenerated input type of OrganizationInvitationRevoke\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of OrganizationInvitationRevoke\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of OrganizationInvitationRevoke.\"\"\"\ntype OrganizationInvitationRevokePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\torganization: Organization!\n\torganizationInvitation: OrganizationInvitation!\n\torganizationInvitationEdge: OrganizationInvitationEdge!\n}\n\ninput OrganizationInvitationSSOInput {\n\tmode: OrganizationMemberSSOModeEnum!\n}\n\n\"\"\"Information about the SSO setup for this invited organization member\"\"\"\ntype OrganizationInvitationSSOType {\n\"\"\"The SSO mode of the invited organization member\"\"\"\n\tmode: OrganizationMemberSSOModeEnum\n}\n\n\"\"\"All the possible states that an organization invitation can be\"\"\"\nenum OrganizationInvitationStates {\n\"\"\"The invitation is waiting for a user to accept it\"\"\"\n\tPENDING\n\"\"\"The invitation was accepted by the person it was sent to\"\"\"\n\tACCEPTED\n\"\"\"The invitation wasn't accepted and the link has expired\"\"\"\n\tEXPIRED\n\"\"\"The invitation was revoked and can no longer be accepted\"\"\"\n\tREVOKED\n}\n\n\"\"\"A team that has been assigned to an invitation\"\"\"\ntype OrganizationInvitationTeamAssignment {\n\tid: ID!\n\"\"\"The role that the user will have once they've accepted the invite\"\"\"\n\trole: TeamMemberRole!\n\"\"\"The team that this assignment refers to\"\"\"\n\tteam: Team!\n}\n\ntype OrganizationInvitationTeamAssignmentConnection implements Connection{\n\tcount: Int!\n\tedges: [OrganizationInvitationTeamAssignmentEdge]\n\tpageInfo: PageInfo\n}\n\ntype OrganizationInvitationTeamAssignmentEdge {\n\tcursor: String!\n\tnode: OrganizationInvitationTeamAssignment\n}\n\n\"\"\"Used to assign teams to organization invitation in mutations\"\"\"\ninput OrganizationInvitationTeamAssignmentInput {\n\"\"\"Used to assign teams to organization invitation in mutations\"\"\"\n\tid: ID!\n\"\"\"Used to assign teams to organization invitation in mutations\"\"\"\n\trole: TeamMemberRole!\n}\n\n\"\"\"A member of an organization\"\"\"\ntype OrganizationMember implements Node{\n\"\"\"Whether or not organizations are required to pay for this user\"\"\"\n\tcomplimentary: Boolean!\n\"\"\"The time when this user was added to the organization\"\"\"\n\tcreatedAt: DateTime!\n\"\"\"The user that added invited this user\"\"\"\n\tcreatedBy: User\n\tid: ID!\n\torganization: Organization!\n\tpermissions: OrganizationMemberPermissions!\n\"\"\"Pipelines the user has access to within the organization\"\"\"\n\tpipelines(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\"\"\"Search within the pipelines the user has access to\"\"\"\n\t\tsearch: String\n\"\"\"Order the pipelines returned\"\"\"\n\t\torder: PipelineOrders\n\t): OrganizationMemberPipelineConnection!\n\"\"\"The users role within the organization\"\"\"\n\trole: OrganizationMemberRole!\n\tsecurity: OrganizationMemberSecurity!\n\tsso: OrganizationMemberSSO!\n\"\"\"Teams that this user is a part of within the organization\"\"\"\n\tteams(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\"\"\"Order the members returned\"\"\"\n\t\torder: TeamMemberOrder\n\t): TeamMemberConnection!\n\tuser: User!\n\"\"\"The public UUID for this organization member\"\"\"\n\tuuid: String!\n}\n\ntype OrganizationMemberConnection implements Connection{\n\tcount: Int!\n\tedges: [OrganizationMemberEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of OrganizationMemberDelete\"\"\"\ninput OrganizationMemberDeleteInput {\n\"\"\"Autogenerated input type of OrganizationMemberDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of OrganizationMemberDelete\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of OrganizationMemberDelete.\"\"\"\ntype OrganizationMemberDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedOrganizationMemberID: ID!\n\torganization: Organization\n\tuser: User\n}\n\ntype OrganizationMemberEdge {\n\tcursor: String!\n\tnode: OrganizationMember\n}\n\n\"\"\"The different orders you can sort members by\"\"\"\nenum OrganizationMemberOrder {\n\"\"\"Order by name alphabetically\"\"\"\n\tNAME\n\"\"\"Order by the most recently created members first\"\"\"\n\tRECENTLY_CREATED\n\"\"\"Order by relevance when searching for members\"\"\"\n\tRELEVANCE\n}\n\n\"\"\"Permissions information about what actions the current user can do against the organization membership record\"\"\"\ntype OrganizationMemberPermissions {\n\"\"\"Whether the user can delete the user from the organization\"\"\"\n\torganizationMemberDelete: Permission\n\"\"\"Whether the user can update the organization's members role information\"\"\"\n\torganizationMemberUpdate: Permission\n}\n\n\"\"\"Represents the connection between a user an a pipeline within an organization\"\"\"\ntype OrganizationMemberPipeline {\n\"\"\"The pipeline the user has access to within the organization\"\"\"\n\tpipeline: Pipeline!\n}\n\ntype OrganizationMemberPipelineConnection implements Connection{\n\tcount: Int!\n\tedges: [OrganizationMemberPipelineEdge]\n\tpageInfo: PageInfo\n}\n\ntype OrganizationMemberPipelineEdge {\n\tcursor: String!\n\tnode: OrganizationMemberPipeline\n}\n\n\"\"\"The roles a user can be within an organization\"\"\"\nenum OrganizationMemberRole {\n\"\"\"The user is a regular member of the organization\"\"\"\n\tMEMBER\n\"\"\"Has full access to the entire organization\"\"\"\n\tADMIN\n}\n\n\"\"\"Information about the SSO setup for this organization member\"\"\"\ntype OrganizationMemberSSO {\n\"\"\"SSO authorizations provided by your organization that have been created for this user\"\"\"\n\tauthorizations(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\"\"\"Filter authorizations by state\"\"\"\n\t\tstate: [SSOAuthorizationState!]\n\t): SSOAuthorizationConnection\n\"\"\"The SSO mode of the organization member\"\"\"\n\tmode: OrganizationMemberSSOModeEnum\n}\n\ninput OrganizationMemberSSOInput {\n\tmode: OrganizationMemberSSOModeEnum!\n}\n\n\"\"\"The SSO authorization modes you can use on a member\"\"\"\nenum OrganizationMemberSSOModeEnum {\n\"\"\"The member must use SSO to access your organization\"\"\"\n\tREQUIRED\n\"\"\"The member can either use SSO or their email & password\"\"\"\n\tOPTIONAL\n}\n\n\"\"\"Information about what security settings the user has enabled in Buildkite\"\"\"\ntype OrganizationMemberSecurity {\n\"\"\"If the user has secured their Buildkite user account with a password\"\"\"\n\tpasswordProtected: Boolean!\n\"\"\"If the user has enabled Two Factor Authentication\"\"\"\n\ttwoFactorEnabled: Boolean!\n}\n\ninput OrganizationMemberSecurityInput {\n\ttwoFactorEnabled: Boolean\n\tpasswordProtected: Boolean\n}\n\n\"\"\"Autogenerated input type of OrganizationMemberUpdate\"\"\"\ninput OrganizationMemberUpdateInput {\n\"\"\"Autogenerated input type of OrganizationMemberUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of OrganizationMemberUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of OrganizationMemberUpdate\"\"\"\n\trole: OrganizationMemberRole\n\"\"\"Autogenerated input type of OrganizationMemberUpdate\"\"\"\n\tsso: OrganizationMemberSSOInput\n}\n\n\"\"\"Autogenerated return type of OrganizationMemberUpdate.\"\"\"\ntype OrganizationMemberUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\torganizationMember: OrganizationMember\n}\n\n\"\"\"Permissions information about what actions the current user can do against the organization\"\"\"\ntype OrganizationPermissions {\n\"\"\"Whether the user can create agent tokens\"\"\"\n\tagentTokenCreate: Permission\n\"\"\"Whether the user can access agent tokens\"\"\"\n\tagentTokenView: Permission\n\"\"\"Whether the user can create a see a list of agents in organization\"\"\"\n\tagentView: Permission\n\"\"\"Whether the user can access audit events for the organization\"\"\"\n\tauditEventsView: Permission\n\"\"\"Whether the user can change the notification services for the organization\"\"\"\n\tnotificationServiceUpdate: Permission\n\"\"\"Whether the user can view and manage billing for the organization\"\"\"\n\torganizationBillingUpdate: Permission\n\"\"\"Whether the user can invite members from an organization\"\"\"\n\torganizationInvitationCreate: Permission\n\"\"\"Whether the user can update/remove members from an organization\"\"\"\n\torganizationMemberUpdate: Permission\n\"\"\"Whether the user can see members in the organization\"\"\"\n\torganizationMemberView: Permission\n\"\"\"Whether the user can see sensitive information about members in the organization\"\"\"\n\torganizationMemberViewSensitive: Permission\n\"\"\"Whether the user can change the organization name and related source code provider settings\"\"\"\n\torganizationUpdate: Permission\n\"\"\"Whether the user can create a new pipeline in the organization\"\"\"\n\tpipelineCreate: Permission\n\"\"\"Whether the user can create a new pipeline without adding it to any teams within the organization\"\"\"\n\tpipelineCreateWithoutTeams: Permission\n\"\"\"Whether the user can create a see a list of pipelines in organization\"\"\"\n\tpipelineView: Permission\n\"\"\"Whether the user can change SSO Providers for the organization\"\"\"\n\tssoProviderCreate: Permission\n\"\"\"Whether the user can change SSO Providers for the organization\"\"\"\n\tssoProviderUpdate: Permission\n\"\"\"Whether the user can create a see a list of suites in organization\"\"\"\n\tsuiteView: Permission\n\"\"\"Whether the user can administer one or all the teams in the organization\"\"\"\n\tteamAdmin: Permission\n\"\"\"Whether the user can create teams for the organization\"\"\"\n\tteamCreate: Permission\n\"\"\"Whether the user can toggle teams on/off for the organization\"\"\"\n\tteamEnabledChange: Permission\n\"\"\"Whether the user can see teams in the organization\"\"\"\n\tteamView: Permission\n}\n\n\"\"\"Autogenerated input type of OrganizationRevokeInactiveTokensAfterUpdateMutation\"\"\"\ninput OrganizationRevokeInactiveTokensAfterUpdateMutationInput {\n\"\"\"Autogenerated input type of OrganizationRevokeInactiveTokensAfterUpdateMutation\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of OrganizationRevokeInactiveTokensAfterUpdateMutation\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of OrganizationRevokeInactiveTokensAfterUpdateMutation\"\"\"\n\trevokeInactiveTokensAfter: RevokeInactiveTokenPeriod!\n}\n\n\"\"\"Autogenerated return type of OrganizationRevokeInactiveTokensAfterUpdateMutation.\"\"\"\ntype OrganizationRevokeInactiveTokensAfterUpdateMutationPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\torganization: Organization\n}\n\n\"\"\"Single sign-on settings for an organization\"\"\"\ntype OrganizationSSO {\n\"\"\"Whether this account is configured for single sign-on\"\"\"\n\tisEnabled: Boolean!\n\"\"\"The single sign-on provider for this organization\"\"\"\n\tprovider: OrganizationSSOProvider\n}\n\n\"\"\"Single sign-on provider information for an organization\"\"\"\ntype OrganizationSSOProvider {\n\tname: String!\n}\n\n\"\"\"Information about pagination in a connection.\"\"\"\ntype PageInfo {\n\"\"\"When paginating forwards, the cursor to continue.\"\"\"\n\tendCursor: String\n\"\"\"When paginating forwards, are there more items?\"\"\"\n\thasNextPage: Boolean!\n\"\"\"When paginating backwards, are there more items?\"\"\"\n\thasPreviousPage: Boolean!\n\"\"\"When paginating backwards, the cursor to continue.\"\"\"\n\tstartCursor: String\n}\n\n\"\"\"The result of checking a permissions\"\"\"\ntype Permission {\n\tallowed: Boolean!\n\tcode: String\n\tmessage: String\n}\n\n\"\"\"A pipeline\"\"\"\ntype Pipeline implements Node{\n\"\"\"Whether existing builds can be rebuilt as new builds.\"\"\"\n\tallowRebuilds: Boolean\n\"\"\"Whether this pipeline has been archived\"\"\"\n\tarchived: Boolean!\n\"\"\"The time when the pipeline was archived\"\"\"\n\tarchivedAt: DateTime\n\"\"\"The user that archived this pipeline\"\"\"\n\tarchivedBy: User\n\"\"\"A branch filter pattern to limit which pushed branches trigger builds on this pipeline.\"\"\"\n\tbranchConfiguration: String\n\"\"\"Returns the builds for this pipeline\"\"\"\n\tbuilds(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t\tstate: [BuildStates!]\n\"\"\"Use `%default` to search by the Pipelines default branch\"\"\"\n\t\tbranch: [String!]\n\t\tcommit: [String!]\n\t\tmetaData: [String!]\n\t\tcreatedAtFrom: DateTime\n\t\tcreatedAtTo: DateTime\n\t): BuildConnection\n\"\"\"When a new build is created on a branch, any previous builds that are running on the same branch will be automatically cancelled\"\"\"\n\tcancelIntermediateBuilds: Boolean!\n\"\"\"Limit which branches build cancelling applies to, for example `!main` will ensure that the main branch won't have it's builds automatically cancelled.\"\"\"\n\tcancelIntermediateBuildsBranchFilter: String\n\tcluster: Cluster\n\"\"\"The color of the pipeline\"\"\"\n\tcolor: String\n\"\"\"The shortest length to which any git commit ID may be truncated while guaranteeing referring to a unique commit\"\"\"\n\tcommitShortLength: Int!\n\"\"\"The time when the pipeline was created\"\"\"\n\tcreatedAt: DateTime\n\"\"\"The user who created the pipeline\"\"\"\n\tcreatedBy: User\n\"\"\"The default branch for this pipeline\"\"\"\n\tdefaultBranch: String\n\"\"\"The default timeout in minutes for all command steps in this pipeline. This can still be overridden in any command step\"\"\"\n\tdefaultTimeoutInMinutes: Int\n\"\"\"The short description of the pipeline\"\"\"\n\tdescription: String\n\"\"\"The emoji of the pipeline\"\"\"\n\temoji: String\n\"\"\"Returns true if the viewer has favorited this pipeline\"\"\"\n\tfavorite: Boolean!\n\tid: ID!\n\tjobs(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t\ttype: [JobTypes!]\n\t\tstate: [JobStates!]\n\t\tpriority: JobPrioritySearch\n\t\tagentQueryRules: [String!]\n\t\tconcurrency: JobConcurrencySearch\n\"\"\"Whether or not the command job passed. Passing `false` will return all failed jobs (including \"soft failed\" jobs)\"\"\"\n\t\tpassed: Boolean\n\"\"\"Filtering jobs based on related step information\"\"\"\n\t\tstep: JobStepSearch\n\"\"\"Order the jobs\"\"\"\n\t\torder: JobOrder\n\t): JobConnection\n\"\"\"The maximum timeout in minutes for all command steps in this pipeline. Any command step without a timeout or with a timeout greater than this value will be set to this value.\"\"\"\n\tmaximumTimeoutInMinutes: Int\n\tmetrics(\n\t\tfirst: Int\n\t\tlast: Int\n\t): PipelineMetricConnection\n\"\"\"The name of the pipeline\"\"\"\n\tname: String!\n\"\"\"The next build number in the sequence\"\"\"\n\tnextBuildNumber: Int!\n\torganization: Organization!\n\tpermissions: PipelinePermissions!\n\tpipelineTemplate: PipelineTemplate\n\"\"\"Whether this pipeline is visible to everyone, including people outside this organization\"\"\"\n\tpublic: Boolean!\n\"\"\"The repository for this pipeline\"\"\"\n\trepository: Repository\n\"\"\"Schedules for this pipeline\"\"\"\n\tschedules(\n\t\tfirst: Int\n\t): PipelineScheduleConnection\n\"\"\"When a new build is created on a branch, any previous builds that haven't yet started on the same branch will be automatically marked as skipped.\"\"\"\n\tskipIntermediateBuilds: Boolean!\n\"\"\"Limit which branches build skipping applies to, for example `!main` will ensure that the main branch won't have it's builds automatically skipped.\"\"\"\n\tskipIntermediateBuildsBranchFilter: String\n\"\"\"The slug of the pipeline\"\"\"\n\tslug: String!\n\tsteps: PipelineSteps\n\"\"\"Tags that have been given to this pipeline\"\"\"\n\ttags: [PipelineTag!]!\n\"\"\"Teams associated with this pipeline\"\"\"\n\tteams(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\"\"\"Search for teams associated that this pipeline is assigned to\"\"\"\n\t\tsearch: String\n\"\"\"Order the pipelines returned\"\"\"\n\t\torder: TeamPipelineOrder\n\t): TeamPipelineConnection\n\"\"\"The URL for the pipeline\"\"\"\n\turl: String!\n\"\"\"The UUID of the pipeline\"\"\"\n\tuuid: String!\n\"\"\"Whether this pipeline is visible to everyone, including people outside this organization\"\"\"\n\tvisibility: PipelineVisibility!\n\"\"\"The URL to use in your repository settings for commit webhooks\"\"\"\n\twebhookURL: String!\n}\n\n\"\"\"The access levels that can be assigned to a pipeline\"\"\"\nenum PipelineAccessLevels {\n\"\"\"Allows edits, builds and reads\"\"\"\n\tMANAGE_BUILD_AND_READ\n\"\"\"Allows builds and read only\"\"\"\n\tBUILD_AND_READ\n\"\"\"Read only - no builds or edits\"\"\"\n\tREAD_ONLY\n}\n\n\"\"\"Autogenerated input type of PipelineArchive\"\"\"\ninput PipelineArchiveInput {\n\"\"\"Autogenerated input type of PipelineArchive\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineArchive\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of PipelineArchive.\"\"\"\ntype PipelineArchivePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tpipeline: Pipeline!\n}\n\ntype PipelineConnection implements Connection{\n\tcount: Int!\n\tedges: [PipelineEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\ninput PipelineCreateInput {\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tname: String!\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tdescription: String\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\temoji: String\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tcolor: String\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tvisibility: PipelineVisibility\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\trepository: PipelineRepositoryInput!\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tsteps: PipelineStepsInput\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tskipIntermediateBuilds: Boolean\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tskipIntermediateBuildsBranchFilter: String\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tcancelIntermediateBuilds: Boolean\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tcancelIntermediateBuildsBranchFilter: String\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tallowRebuilds: Boolean\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tdefaultTimeoutInMinutes: Int\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tmaximumTimeoutInMinutes: Int\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tteams: [PipelineTeamAssignmentInput!]\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tdefaultBranch: String\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tnextBuildNumber: Int\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tclusterId: ID\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tpipelineTemplateId: ID\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\ttags: [PipelineTagInput!]\n\"\"\"Autogenerated input type of PipelineCreate\"\"\"\n\tbranchConfiguration: String\n}\n\n\"\"\"Autogenerated return type of PipelineCreate.\"\"\"\ntype PipelineCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tcluster: Cluster\n\torganization: Organization!\n\tpipeline: Pipeline!\n\tpipelineEdge: PipelineEdge!\n\tpipelineTemplate: PipelineTemplate\n}\n\n\"\"\"Autogenerated input type of PipelineCreateWebhook\"\"\"\ninput PipelineCreateWebhookInput {\n\"\"\"Autogenerated input type of PipelineCreateWebhook\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineCreateWebhook\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of PipelineCreateWebhook.\"\"\"\ntype PipelineCreateWebhookPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tpipelineID: ID!\n}\n\n\"\"\"Autogenerated input type of PipelineDelete\"\"\"\ninput PipelineDeleteInput {\n\"\"\"Autogenerated input type of PipelineDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineDelete\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of PipelineDelete.\"\"\"\ntype PipelineDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedPipelineID: ID!\n\torganization: Organization!\n}\n\ntype PipelineEdge {\n\tcursor: String!\n\tnode: Pipeline\n}\n\n\"\"\"Autogenerated input type of PipelineFavorite\"\"\"\ninput PipelineFavoriteInput {\n\"\"\"Autogenerated input type of PipelineFavorite\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineFavorite\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of PipelineFavorite\"\"\"\n\tfavorite: Boolean!\n}\n\n\"\"\"Autogenerated return type of PipelineFavorite.\"\"\"\ntype PipelineFavoritePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tpipeline: Pipeline\n}\n\n\"\"\"A metric for a pipeline\"\"\"\ntype PipelineMetric implements Node{\n\tid: ID!\n\"\"\"The label of this metric\"\"\"\n\tlabel: ID!\n\"\"\"The URL for this metric\"\"\"\n\turl: String\n\"\"\"The value for this metric\"\"\"\n\tvalue: String\n}\n\ntype PipelineMetricConnection implements Connection{\n\tcount: Int!\n\tedges: [PipelineMetricEdge]\n\tpageInfo: PageInfo\n}\n\ntype PipelineMetricEdge {\n\tcursor: String!\n\tnode: PipelineMetric\n}\n\n\"\"\"The different orders you can sort pipelines by\"\"\"\nenum PipelineOrders {\n\"\"\"Order by name alphabetically\"\"\"\n\tNAME\n\"\"\"Order by favorites first alphabetically, then the rest of the pipelines alphabetically\"\"\"\n\tNAME_WITH_FAVORITES_FIRST\n\"\"\"Order by the most recently created pipelines first\"\"\"\n\tRECENTLY_CREATED\n\"\"\"Order by relevance when searching for pipelines\"\"\"\n\tRELEVANCE\n}\n\n\"\"\"Permission information about what actions the current user can do against the pipeline\"\"\"\ntype PipelinePermissions {\n\"\"\"Whether the user can create builds on this pipeline\"\"\"\n\tbuildCreate: Permission!\n\"\"\"Whether the user can delete this pipeline\"\"\"\n\tpipelineDelete: Permission!\n\"\"\"Whether the user can favorite this pipeline\"\"\"\n\tpipelineFavorite: Permission!\n\"\"\"Whether the user can create schedules on this pipeline\"\"\"\n\tpipelineScheduleCreate: Permission!\n\"\"\"Whether the user can edit the settings of this pipeline\"\"\"\n\tpipelineUpdate: Permission!\n}\n\n\"\"\"Repository information for a pipeline\"\"\"\ninput PipelineRepositoryInput {\n\"\"\"Repository information for a pipeline\"\"\"\n\turl: String!\n}\n\n\"\"\"Autogenerated input type of PipelineRotateWebhookURL\"\"\"\ninput PipelineRotateWebhookURLInput {\n\"\"\"Autogenerated input type of PipelineRotateWebhookURL\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineRotateWebhookURL\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of PipelineRotateWebhookURL.\"\"\"\ntype PipelineRotateWebhookURLPayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tpipeline: Pipeline!\n}\n\n\"\"\"A schedule of when a build should automatically triggered for a Pipeline\"\"\"\ntype PipelineSchedule implements Node{\n\"\"\"The branch to use for builds that this schedule triggers. Defaults to to the default branch in the Pipeline\"\"\"\n\tbranch: String\n\"\"\"Returns the builds created by this schedule\"\"\"\n\tbuilds(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t): BuildConnection\n\"\"\"The commit to use for builds that this schedule triggers. Defaults to `HEAD`\"\"\"\n\tcommit: String\n\"\"\"The time when this schedule was created\"\"\"\n\tcreatedAt: DateTime\n\tcreatedBy: User\n\"\"\"A definition of the trigger build schedule in cron syntax\"\"\"\n\tcronline: String!\n\"\"\"If this Pipeline schedule is currently enabled\"\"\"\n\tenabled: Boolean\n\"\"\"Environment variables passed to any triggered builds\"\"\"\n\tenv: [String!]\n\"\"\"The time when this schedule failed\"\"\"\n\tfailedAt: DateTime\n\"\"\"If the last attempt at triggering this scheduled build fails, this will be the reason\"\"\"\n\tfailedMessage: String\n\tid: ID!\n\"\"\"A short description of the Pipeline schedule\"\"\"\n\tlabel: String!\n\"\"\"The message to use for builds that this schedule triggers\"\"\"\n\tmessage: String\n\"\"\"The time when this schedule will create a build next\"\"\"\n\tnextBuildAt: DateTime\n\tpermissions: PipelineSchedulePermissions!\n\tpipeline: Pipeline\n\"\"\"The UUID of the Pipeline schedule\"\"\"\n\tuuid: String!\n}\n\ntype PipelineScheduleConnection implements Connection{\n\tcount: Int!\n\tedges: [PipelineScheduleEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of PipelineScheduleCreate\"\"\"\ninput PipelineScheduleCreateInput {\n\"\"\"Autogenerated input type of PipelineScheduleCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineScheduleCreate\"\"\"\n\tpipelineID: ID!\n\"\"\"Autogenerated input type of PipelineScheduleCreate\"\"\"\n\tlabel: String\n\"\"\"Autogenerated input type of PipelineScheduleCreate\"\"\"\n\tcronline: String\n\"\"\"Autogenerated input type of PipelineScheduleCreate\"\"\"\n\tmessage: String\n\"\"\"Autogenerated input type of PipelineScheduleCreate\"\"\"\n\tcommit: String\n\"\"\"Autogenerated input type of PipelineScheduleCreate\"\"\"\n\tbranch: String\n\"\"\"Autogenerated input type of PipelineScheduleCreate\"\"\"\n\tenv: String\n\"\"\"Autogenerated input type of PipelineScheduleCreate\"\"\"\n\tenabled: Boolean\n}\n\n\"\"\"Autogenerated return type of PipelineScheduleCreate.\"\"\"\ntype PipelineScheduleCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tpipeline: Pipeline!\n\tpipelineScheduleEdge: PipelineScheduleEdge!\n}\n\n\"\"\"Autogenerated input type of PipelineScheduleDelete\"\"\"\ninput PipelineScheduleDeleteInput {\n\"\"\"Autogenerated input type of PipelineScheduleDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineScheduleDelete\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of PipelineScheduleDelete.\"\"\"\ntype PipelineScheduleDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedPipelineScheduleID: ID!\n\tpipeline: Pipeline\n}\n\ntype PipelineScheduleEdge {\n\tcursor: String!\n\tnode: PipelineSchedule\n}\n\n\"\"\"Permission information about what actions the current user can do against the pipeline schedule\"\"\"\ntype PipelineSchedulePermissions {\n\"\"\"Whether the user can delete the schedule\"\"\"\n\tpipelineScheduleDelete: Permission\n\"\"\"Whether the user can update the schedule\"\"\"\n\tpipelineScheduleUpdate: Permission\n}\n\n\"\"\"Autogenerated input type of PipelineScheduleUpdate\"\"\"\ninput PipelineScheduleUpdateInput {\n\"\"\"Autogenerated input type of PipelineScheduleUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineScheduleUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of PipelineScheduleUpdate\"\"\"\n\tlabel: String\n\"\"\"Autogenerated input type of PipelineScheduleUpdate\"\"\"\n\tcronline: String\n\"\"\"Autogenerated input type of PipelineScheduleUpdate\"\"\"\n\tmessage: String\n\"\"\"Autogenerated input type of PipelineScheduleUpdate\"\"\"\n\tcommit: String\n\"\"\"Autogenerated input type of PipelineScheduleUpdate\"\"\"\n\tbranch: String\n\"\"\"Autogenerated input type of PipelineScheduleUpdate\"\"\"\n\tenv: String\n\"\"\"Autogenerated input type of PipelineScheduleUpdate\"\"\"\n\tenabled: Boolean\n}\n\n\"\"\"Autogenerated return type of PipelineScheduleUpdate.\"\"\"\ntype PipelineScheduleUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tpipelineSchedule: PipelineSchedule!\n}\n\n\"\"\"A Pipeline identifier using a slug, and optionally negated with a leading `!`\"\"\"\nscalar PipelineSelector\n\n\"\"\"Steps defined on a pipeline\"\"\"\ntype PipelineSteps {\n\"\"\"A YAML representation of the pipeline steps\"\"\"\n\tyaml: YAML\n}\n\n\"\"\"Step definition for a pipeline\"\"\"\ninput PipelineStepsInput {\n\"\"\"Step definition for a pipeline\"\"\"\n\tyaml: String!\n}\n\n\"\"\"A tag associated with a pipeline\"\"\"\ntype PipelineTag {\n\"\"\"The label for this tag\"\"\"\n\tlabel: String!\n}\n\n\"\"\"Tag associated with a pipeline\"\"\"\ninput PipelineTagInput {\n\"\"\"Tag associated with a pipeline\"\"\"\n\tlabel: String!\n}\n\n\"\"\"Used to assign teams to pipelines\"\"\"\ninput PipelineTeamAssignmentInput {\n\"\"\"Used to assign teams to pipelines\"\"\"\n\tid: ID!\n\"\"\"Used to assign teams to pipelines\"\"\"\n\taccessLevel: PipelineAccessLevels\n}\n\n\"\"\"A template defining a fixed step configuration for a pipeline\"\"\"\ntype PipelineTemplate implements Node{\n\"\"\"If the pipeline template is available for assignment by non admin users\"\"\"\n\tavailable: Boolean!\n\"\"\"A YAML representation of the step configuration\"\"\"\n\tconfiguration: YAML!\n\"\"\"The time when the template was created\"\"\"\n\tcreatedAt: DateTime!\n\"\"\"The user who created the template\"\"\"\n\tcreatedBy: User!\n\"\"\"The short description of the template\"\"\"\n\tdescription: String\n\tid: ID!\n\"\"\"The name of the template\"\"\"\n\tname: String!\n\"\"\"The last time the template was changed\"\"\"\n\tupdatedAt: DateTime!\n\"\"\"The user who last updated the template\"\"\"\n\tupdatedBy: User!\n\"\"\"The UUID for the template\"\"\"\n\tuuid: ID!\n}\n\ntype PipelineTemplateConnection implements Connection{\n\tcount: Int!\n\tedges: [PipelineTemplateEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of PipelineTemplateCreate\"\"\"\ninput PipelineTemplateCreateInput {\n\"\"\"Autogenerated input type of PipelineTemplateCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineTemplateCreate\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of PipelineTemplateCreate\"\"\"\n\tname: String!\n\"\"\"Autogenerated input type of PipelineTemplateCreate\"\"\"\n\tdescription: String\n\"\"\"Autogenerated input type of PipelineTemplateCreate\"\"\"\n\tconfiguration: String!\n\"\"\"Autogenerated input type of PipelineTemplateCreate\"\"\"\n\tavailable: Boolean\n}\n\n\"\"\"Autogenerated return type of PipelineTemplateCreate.\"\"\"\ntype PipelineTemplateCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tpipelineTemplate: PipelineTemplate!\n}\n\n\"\"\"Autogenerated input type of PipelineTemplateDelete\"\"\"\ninput PipelineTemplateDeleteInput {\n\"\"\"Autogenerated input type of PipelineTemplateDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineTemplateDelete\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of PipelineTemplateDelete\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of PipelineTemplateDelete.\"\"\"\ntype PipelineTemplateDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedPipelineTemplateId: ID!\n}\n\ntype PipelineTemplateEdge {\n\tcursor: String!\n\tnode: PipelineTemplate\n}\n\n\"\"\"The different orders you can sort pipeline templates by\"\"\"\nenum PipelineTemplateOrder {\n\"\"\"Order by name alphabetically\"\"\"\n\tNAME\n\"\"\"Order by the most recently created pipeline templates first\"\"\"\n\tRECENTLY_CREATED\n}\n\n\"\"\"Autogenerated input type of PipelineTemplateUpdate\"\"\"\ninput PipelineTemplateUpdateInput {\n\"\"\"Autogenerated input type of PipelineTemplateUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineTemplateUpdate\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of PipelineTemplateUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of PipelineTemplateUpdate\"\"\"\n\tname: String\n\"\"\"Autogenerated input type of PipelineTemplateUpdate\"\"\"\n\tdescription: String\n\"\"\"Autogenerated input type of PipelineTemplateUpdate\"\"\"\n\tconfiguration: String\n\"\"\"Autogenerated input type of PipelineTemplateUpdate\"\"\"\n\tavailable: Boolean\n}\n\n\"\"\"Autogenerated return type of PipelineTemplateUpdate.\"\"\"\ntype PipelineTemplateUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tpipelineTemplate: PipelineTemplate!\n}\n\n\"\"\"Autogenerated input type of PipelineUnarchive\"\"\"\ninput PipelineUnarchiveInput {\n\"\"\"Autogenerated input type of PipelineUnarchive\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineUnarchive\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of PipelineUnarchive.\"\"\"\ntype PipelineUnarchivePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tpipeline: Pipeline!\n}\n\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\ninput PipelineUpdateInput {\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tname: String\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tdescription: String\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\temoji: String\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tcolor: String\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tvisibility: PipelineVisibility\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\trepository: PipelineRepositoryInput\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tsteps: PipelineStepsInput\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tdefaultBranch: String\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tnextBuildNumber: Int\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tskipIntermediateBuilds: Boolean\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tskipIntermediateBuildsBranchFilter: String\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tcancelIntermediateBuilds: Boolean\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tcancelIntermediateBuildsBranchFilter: String\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tallowRebuilds: Boolean\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tdefaultTimeoutInMinutes: Int\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tmaximumTimeoutInMinutes: Int\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tclusterId: ID\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tpipelineTemplateId: ID\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tarchived: Boolean\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\ttags: [PipelineTagInput!]\n\"\"\"Autogenerated input type of PipelineUpdate\"\"\"\n\tbranchConfiguration: String\n}\n\n\"\"\"Autogenerated return type of PipelineUpdate.\"\"\"\ntype PipelineUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tpipeline: Pipeline!\n}\n\n\"\"\"The visibility of the pipeline\"\"\"\nenum PipelineVisibility {\n\"\"\"The pipeline is public\"\"\"\n\tPUBLIC\n\"\"\"The pipeline is private\"\"\"\n\tPRIVATE\n}\n\n\"\"\"A pull request on a provider\"\"\"\ntype PullRequest {\n\tid: String!\n}\n\n\"\"\"The query root for this schema\"\"\"\ntype Query {\n\"\"\"Find an agent by its slug\"\"\"\n\tagent(\n\"\"\"The UUID for the agent, prefixed by its organization's slug i.e. `acme-inc/0bd5ea7c-89b3-4f40-8ca3-ffac805771eb`\"\"\"\n\t\tslug: ID!\n\t): Agent\n\"\"\"Find an agent token by its slug\"\"\"\n\tagentToken(\n\"\"\"The UUID for the agent token, prefixed by its organization's slug i.e. `acme-inc/0bd5ea7c-89b3-4f40-8ca3-ffac805771eb`\"\"\"\n\t\tslug: ID!\n\t): AgentToken\n\"\"\"Find a API Access Token code\"\"\"\n\tapiAccessTokenCode(\n\"\"\"The code provided by the Auth API\"\"\"\n\t\tcode: ID!\n\t): APIAccessTokenCode\n\"\"\"Find an artifact by its UUID\"\"\"\n\tartifact(\n\t\tuuid: ID!\n\t): Artifact\n\"\"\"Find an audit event via its uuid\"\"\"\n\tauditEvent(\n\"\"\"The UUID for the audit event i.e. `0bd5ea7c-89b3-4f40-8ca3-ffac805771eb`\"\"\"\n\t\tuuid: ID!\n\t): AuditEvent\n\"\"\"Find a build\"\"\"\n\tbuild(\n\"\"\"The number of the build, prefixed with its organization and pipeline. i.e. `acme-inc/my-pipeline/123`\"\"\"\n\t\tslug: ID\n\"\"\"The UUID of the build\"\"\"\n\t\tuuid: ID\n\t): Build\n\"\"\"Find a GraphQL snippet\"\"\"\n\tgraphQLSnippet(\n\"\"\"The UUID for this GraphQL snippet\"\"\"\n\t\tuuid: String!\n\t): GraphQLSnippet\n\"\"\"Find a build job\"\"\"\n\tjob(\n\t\tuuid: ID!\n\t): Job\n\"\"\"Fetches an object given its ID.\"\"\"\n\tnode(\n\"\"\"ID of the object.\"\"\"\n\t\tid: ID!\n\t): Node\n\"\"\"Find a notification service via its UUID\"\"\"\n\tnotificationService(\n\"\"\"The UUID for the notification service i.e. `0bd5ea7c-89b3-4f40-8ca3-ffac805771eb`\"\"\"\n\t\tuuid: ID!\n\t): NotificationService\n\"\"\"Find an organization\"\"\"\n\torganization(\n\"\"\"The slug of the organization\"\"\"\n\t\tslug: ID\n\"\"\"The UUID of the organization\"\"\"\n\t\tuuid: ID\n\t): Organization\n\"\"\"Find an organization invitation via its slug\"\"\"\n\torganizationInvitation(\n\"\"\"The UUID for the invitation, prefixed by its organization's slug i.e. `acme-inc/0bd5ea7c-89b3-4f40-8ca3-ffac805771eb`\"\"\"\n\t\tslug: ID!\n\t): OrganizationInvitation\n\"\"\"Find an organization membership via its slug\"\"\"\n\torganizationMember(\n\"\"\"The UUID for the membership, prefixed by its organization's slug i.e. `acme-inc/0bd5ea7c-89b3-4f40-8ca3-ffac805771eb`\"\"\"\n\t\tslug: ID!\n\t): OrganizationMember\n\"\"\"Find a pipeline\"\"\"\n\tpipeline(\n\"\"\"The slug of the pipeline, prefixed with its organization. i.e. `acme-inc/my-pipeline`\"\"\"\n\t\tslug: ID\n\"\"\"The UUID of the pipeline\"\"\"\n\t\tuuid: ID\n\t): Pipeline\n\"\"\"Find a pipeline schedule by its slug\"\"\"\n\tpipelineSchedule(\n\"\"\"The UUID for the pipeline schedule, prefixed by its organization and pipeline's slug i.e. `acme-inc/my-pipeline/0bd5ea7c-89b3-4f40-8ca3-ffac805771eb`\"\"\"\n\t\tslug: ID!\n\t): PipelineSchedule\n\"\"\"Find a pipeline template\"\"\"\n\tpipelineTemplate(\n\"\"\"The UUID of the pipeline template\"\"\"\n\t\tuuid: ID!\n\t): PipelineTemplate\n\"\"\"Find a secret via its uuid. This does not contain the value of the secret or encrypted material.\"\"\"\n\tsecret(\n\"\"\"The UUID for the secret i.e. `0bd5ea7c-89b3-4f40-8ca3-ffac805771eb`\"\"\"\n\t\tuuid: ID!\n\t): Secret\n\"\"\"Find an sso provider either using it's slug, or UUID\"\"\"\n\tssoProvider(\n\"\"\"The slug for the sso provider, prefixed by its organization's slug i.e. `acme-inc/0bd5ea7c-89b3-4f40-8ca3-ffac805771eb`\"\"\"\n\t\tslug: ID\n\"\"\"The UUID of the sso provider\"\"\"\n\t\tuuid: ID\n\t): SSOProvider\n\"\"\"Find a team\"\"\"\n\tteam(\n\"\"\"The slug of the team, prefixed with its organization. i.e. `acme-inc/awesome-team`\"\"\"\n\t\tslug: ID!\n\t): Team\n\"\"\"Context of the current user using the GraphQL API\"\"\"\n\tviewer: Viewer\n}\n\n\"\"\"A recovery code\"\"\"\ntype RecoveryCode {\n\"\"\"The recovery code.\"\"\"\n\tcode: String!\n\"\"\"Whether the recovery codes is used\"\"\"\n\tconsumed: Boolean!\n\"\"\"Foo\"\"\"\n\tconsumedAt: String\n}\n\n\"\"\"A batch of recovery codes\"\"\"\ntype RecoveryCodeBatch {\n\"\"\"Whether the batch of recovery codes is active\"\"\"\n\tactive: Boolean!\n\"\"\"The recovery codes from this batch. Codes are consumed when used, and codes will be included in this list whether consumed or not\"\"\"\n\tcodes: [RecoveryCode!]!\n\tid: ID!\n}\n\n\"\"\"A repository associated with a pipeline\"\"\"\ntype Repository {\n\"\"\"The repository’s provider\"\"\"\n\tprovider: RepositoryProvider\n\"\"\"The git URL for this repository\"\"\"\n\turl: String!\n}\n\ninterface RepositoryProvider {\n\tname: String!\n\turl: String\n\twebhookUrl: String\n}\n\n\"\"\"A pipeline's repository is being provided by Beanstalk\"\"\"\ntype RepositoryProviderBeanstalk implements RepositoryProvider{\n\"\"\"The name of the provider\"\"\"\n\tname: String!\n\"\"\"This URL to the provider’s web interface\"\"\"\n\turl: String\n\"\"\"The URL to use when setting up webhooks from the provider to trigger Buildkite builds\"\"\"\n\twebhookUrl: String\n}\n\n\"\"\"A pipeline's repository is being provided by Bitbucket\"\"\"\ntype RepositoryProviderBitbucket implements RepositoryProvider{\n\"\"\"The name of the provider\"\"\"\n\tname: String!\n\"\"\"This URL to the provider’s web interface\"\"\"\n\turl: String\n\"\"\"The URL to use when setting up webhooks from the provider to trigger Buildkite builds\"\"\"\n\twebhookUrl: String\n}\n\n\"\"\"A pipeline's repository is being provided by Bitbucket Server\"\"\"\ntype RepositoryProviderBitbucketServer implements RepositoryProvider{\n\"\"\"The name of the provider\"\"\"\n\tname: String!\n\"\"\"This URL to the provider’s web interface\"\"\"\n\turl: String\n\"\"\"The URL to use when setting up webhooks from the provider to trigger Buildkite builds\"\"\"\n\twebhookUrl: String\n}\n\n\"\"\"A pipeline's repository is being provided by Codebase\"\"\"\ntype RepositoryProviderCodebase implements RepositoryProvider{\n\"\"\"The name of the provider\"\"\"\n\tname: String!\n\"\"\"This URL to the provider’s web interface\"\"\"\n\turl: String\n\"\"\"The URL to use when setting up webhooks from the provider to trigger Buildkite builds\"\"\"\n\twebhookUrl: String\n}\n\n\"\"\"A pipeline's repository is being provided by GitHub\"\"\"\ntype RepositoryProviderGithub implements RepositoryProvider{\n\"\"\"The name of the provider\"\"\"\n\tname: String!\n\"\"\"This URL to the provider’s web interface\"\"\"\n\turl: String\n\"\"\"The URL to use when setting up webhooks from the provider to trigger Buildkite builds\"\"\"\n\twebhookUrl: String\n}\n\n\"\"\"A pipeline's repository is being provided by GitHub Enterprise\"\"\"\ntype RepositoryProviderGithubEnterprise implements RepositoryProvider{\n\"\"\"The name of the provider\"\"\"\n\tname: String!\n\"\"\"This URL to the provider’s web interface\"\"\"\n\turl: String\n\"\"\"The URL to use when setting up webhooks from the provider to trigger Buildkite builds\"\"\"\n\twebhookUrl: String\n}\n\n\"\"\"A pipeline's repository is being provided by GitLab\"\"\"\ntype RepositoryProviderGitlab implements RepositoryProvider{\n\"\"\"The name of the provider\"\"\"\n\tname: String!\n\"\"\"This URL to the provider’s web interface\"\"\"\n\turl: String\n\"\"\"The URL to use when setting up webhooks from the provider to trigger Buildkite builds\"\"\"\n\twebhookUrl: String\n}\n\n\"\"\"A pipeline's repository is being provided by GitLab Community Edition\"\"\"\ntype RepositoryProviderGitlabCommunity implements RepositoryProvider{\n\"\"\"The name of the provider\"\"\"\n\tname: String!\n\"\"\"This URL to the provider’s web interface\"\"\"\n\turl: String\n\"\"\"The URL to use when setting up webhooks from the provider to trigger Buildkite builds\"\"\"\n\twebhookUrl: String\n}\n\n\"\"\"A pipeline's repository is being provided by GitLab Enterprise Edition\"\"\"\ntype RepositoryProviderGitlabEnterprise implements RepositoryProvider{\n\"\"\"The name of the provider\"\"\"\n\tname: String!\n\"\"\"This URL to the provider’s web interface\"\"\"\n\turl: String\n\"\"\"The URL to use when setting up webhooks from the provider to trigger Buildkite builds\"\"\"\n\twebhookUrl: String\n}\n\n\"\"\"A pipeline's repository is being provided by a service unknown to Buildkite\"\"\"\ntype RepositoryProviderUnknown implements RepositoryProvider{\n\"\"\"The name of the provider\"\"\"\n\tname: String!\n\"\"\"This URL to the provider’s web interface\"\"\"\n\turl: String\n\"\"\"The URL to use when setting up webhooks from the provider to trigger Buildkite builds\"\"\"\n\twebhookUrl: String\n}\n\n\"\"\"An aggregate of resource usage, grouped by day and resource.\"\"\"\ninterface ResourceUsageInterface {\n\"\"\"An aggregate of resource usage, grouped by day and resource.\"\"\"\n\taggregatedOn: ISO8601Date!\n}\n\n\"\"\"All types of billable resources\"\"\"\nenum ResourceUsageType {\n\"\"\"These records represent a pipeline's job minutes usage for a single day\"\"\"\n\tJOB_MINUTES\n\"\"\"These records represent a suite's test executions usage for a single day\"\"\"\n\tTEST_EXECUTIONS\n}\n\n\"\"\"API tokens with access to this organization will be automatically revoked after this many days of inactivity.\"\"\"\nenum RevokeInactiveTokenPeriod {\n\"\"\"Revoke organization access from API tokens after 30 days of inactivity\"\"\"\n\tDAYS_30\n\"\"\"Revoke organization access from API tokens after 60 days of inactivity\"\"\"\n\tDAYS_60\n\"\"\"Revoke organization access from API tokens after 90 days of inactivity\"\"\"\n\tDAYS_90\n\"\"\"Revoke organization access from API tokens after 180 days of inactivity\"\"\"\n\tDAYS_180\n\"\"\"Revoke organization access from API tokens after 365 days of inactivity\"\"\"\n\tDAYS_365\n\"\"\"Never revoke organization access from inactive API tokens\"\"\"\n\tNEVER\n}\n\ntype SCMPipelineSettings {\n\tid: ID!\n}\n\ntype SCMRepositoryHost {\n\tid: ID!\n}\n\ntype SCMService {\n\tid: ID!\n}\n\ntype SSOAuthorization {\n\"\"\"The time when this SSO Authorization was created\"\"\"\n\tcreatedAt: DateTime!\n\"\"\"The time when this SSO Authorization was expired\"\"\"\n\texpiredAt: DateTime\n\tid: ID!\n\"\"\"Details around the identity provided by the SSO provider\"\"\"\n\tidentity: SSOAuthorizationIdentity\n\"\"\"The time when this SSO Authorization was manually revoked\"\"\"\n\trevokedAt: DateTime\n\"\"\"The SSO provider associated with this authorization\"\"\"\n\tssoProvider: SSOProvider!\n\"\"\"The current state of the SSO Authorization\"\"\"\n\tstate: SSOAuthorizationState!\n\"\"\"The user associated with this authorization\"\"\"\n\tuser: User\n\"\"\"The time when this SSO Authorization was destroyed because the user logged out\"\"\"\n\tuserSessionDestroyedAt: DateTime\n\"\"\"The public UUID for this SSO authorization\"\"\"\n\tuuid: String!\n}\n\ntype SSOAuthorizationConnection implements Connection{\n\tcount: Int!\n\tedges: [SSOAuthorizationEdge]\n\tpageInfo: PageInfo\n}\n\ntype SSOAuthorizationEdge {\n\tcursor: String!\n\tnode: SSOAuthorization\n}\n\ntype SSOAuthorizationIdentity {\n\"\"\"The avatar URL provided in this identity\"\"\"\n\tavatarURL: String\n\"\"\"The email addresses provided in this identity\"\"\"\n\temail: String\n\"\"\"The name provided in this identity\"\"\"\n\tname: String\n\"\"\"The identifier provided in this identity\"\"\"\n\tuid: String\n}\n\n\"\"\"All the possible states an SSO Authorization\"\"\"\nenum SSOAuthorizationState {\n\"\"\"The authorization has been verified and is in use\"\"\"\n\tVERIFIED\n\"\"\"The authorization was verified but has since been destroyed as the user logged out of that session\"\"\"\n\tVERIFIED_USER_SESSION_DESTROYED\n\"\"\"The authorization was verified but has since been manually revoked\"\"\"\n\tVERIFIED_REVOKED\n\"\"\"The authorization was verified but has since expired\"\"\"\n\tVERIFIED_EXPIRED\n}\n\ninterface SSOProvider {\n\tcreatedAt: DateTime!\n\tcreatedBy: User!\n\tdisabledAt: DateTime\n\tdisabledBy: User\n\tdisabledReason: String\n\temailDomain: String\n\temailDomainVerificationAddress: String\n\temailDomainVerifiedAt: DateTime\n\tenabledAt: DateTime\n\tenabledBy: User\n\tid: ID!\n\tnote: String\n\torganization: Organization\n\tpinSessionToIpAddress: Boolean\n\tsessionDurationInHours: Int\n\tstate: SSOProviderStates!\n\ttestAuthorizationRequired: Boolean\n\ttype: SSOProviderTypes!\n\turl: String!\n\tuuid: ID!\n}\n\ntype SSOProviderConnection implements Connection{\n\tcount: Int!\n\tedges: [SSOProviderEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\ninput SSOProviderCreateInput {\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\torganizationId: ID!\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\ttype: SSOProviderTypes!\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\tnote: String\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\tsessionDurationInHours: Int\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\tpinSessionToIpAddress: Boolean\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\temailDomain: String\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\temailDomainVerificationAddress: String\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\tidentityProvider: SSOProviderSAMLIdP\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\tdigestMethod: SSOProviderSAMLXMLSecurity\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\tsignatureMethod: SSOProviderSAMLRSAXMLSecurity\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\tgithubOrganizationName: String\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\tgoogleHostedDomain: String\n\"\"\"Autogenerated input type of SSOProviderCreate\"\"\"\n\tdiscloseGoogleHostedDomain: Boolean\n}\n\n\"\"\"Autogenerated return type of SSOProviderCreate.\"\"\"\ntype SSOProviderCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\torganization: Organization!\n\tssoProvider: SSOProvider!\n\tssoProviderEdge: SSOProviderEdge!\n}\n\n\"\"\"Autogenerated input type of SSOProviderDelete\"\"\"\ninput SSOProviderDeleteInput {\n\"\"\"Autogenerated input type of SSOProviderDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of SSOProviderDelete\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of SSOProviderDelete.\"\"\"\ntype SSOProviderDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedSSOProviderId: ID!\n\torganization: Organization!\n}\n\n\"\"\"Autogenerated input type of SSOProviderDisable\"\"\"\ninput SSOProviderDisableInput {\n\"\"\"Autogenerated input type of SSOProviderDisable\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of SSOProviderDisable\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of SSOProviderDisable\"\"\"\n\tdisabledReason: String\n}\n\n\"\"\"Autogenerated return type of SSOProviderDisable.\"\"\"\ntype SSOProviderDisablePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tssoProvider: SSOProvider!\n}\n\ntype SSOProviderEdge {\n\tcursor: String!\n\tnode: SSOProvider\n}\n\n\"\"\"Autogenerated input type of SSOProviderEnable\"\"\"\ninput SSOProviderEnableInput {\n\"\"\"Autogenerated input type of SSOProviderEnable\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of SSOProviderEnable\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of SSOProviderEnable.\"\"\"\ntype SSOProviderEnablePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tssoProvider: SSOProvider!\n}\n\n\"\"\"Single sign-on provided by GitHub\"\"\"\ntype SSOProviderGitHubApp implements Node & SSOProvider{\n\"\"\"The time when this SSO Provider was created\"\"\"\n\tcreatedAt: DateTime!\n\"\"\"The user that created this SSO Provider\"\"\"\n\tcreatedBy: User!\n\"\"\"The time when this SSO Provider was disabled\"\"\"\n\tdisabledAt: DateTime\n\"\"\"The user that disabled this SSO Provider\"\"\"\n\tdisabledBy: User\n\"\"\"The reason this SSO Provider was disabled\"\"\"\n\tdisabledReason: String\n\"\"\"An email domain whose addresses should be offered this SSO Provider during login.\"\"\"\n\temailDomain: String\n\temailDomainVerificationAddress: String\n\temailDomainVerifiedAt: DateTime\n\"\"\"The time when this SSO Provider was enabled\"\"\"\n\tenabledAt: DateTime\n\"\"\"The user that enabled this SSO Provider\"\"\"\n\tenabledBy: User\n\"\"\"The name of the organization on GitHub that the user must be in for an SSO authorization to be verified\"\"\"\n\tgithubOrganizationName: String!\n\tid: ID!\n\"\"\"An extra message that can be added the Authorization screen of an SSO Provider\"\"\"\n\tnote: String\n\torganization: Organization\n\"\"\"Defaults to false. If true, users are required to re-authenticate when their IP address changes.\"\"\"\n\tpinSessionToIpAddress: Boolean\n\"\"\"How long a session should last before requiring re-authorization. A `null` value indicates an infinite session.\"\"\"\n\tsessionDurationInHours: Int\n\"\"\"The current state of the SSO Provider\"\"\"\n\tstate: SSOProviderStates!\n\"\"\"Whether the SSO Provider requires a test authorization. If true, the provider can not yet be activated.\"\"\"\n\ttestAuthorizationRequired: Boolean\n\"\"\"The type of SSO Provider\"\"\"\n\ttype: SSOProviderTypes!\n\"\"\"The authorization URL for this SSO Provider\"\"\"\n\turl: String!\n\"\"\"The UUID for this SSO Provider\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"Single sign-on provided by Google\"\"\"\ntype SSOProviderGoogleGSuite implements Node & SSOProvider{\n\"\"\"The time when this SSO Provider was created\"\"\"\n\tcreatedAt: DateTime!\n\"\"\"The user that created this SSO Provider\"\"\"\n\tcreatedBy: User!\n\"\"\"The time when this SSO Provider was disabled\"\"\"\n\tdisabledAt: DateTime\n\"\"\"The user that disabled this SSO Provider\"\"\"\n\tdisabledBy: User\n\"\"\"The reason this SSO Provider was disabled\"\"\"\n\tdisabledReason: String\n\"\"\"Whether or not the hosted domain should be presented to the user during SSO\"\"\"\n\tdiscloseGoogleHostedDomain: Boolean!\n\"\"\"An email domain whose addresses should be offered this SSO Provider during login.\"\"\"\n\temailDomain: String\n\temailDomainVerificationAddress: String\n\temailDomainVerifiedAt: DateTime\n\"\"\"The time when this SSO Provider was enabled\"\"\"\n\tenabledAt: DateTime\n\"\"\"The user that enabled this SSO Provider\"\"\"\n\tenabledBy: User\n\"\"\"The Google hosted domain that is required to be present in OAuth\"\"\"\n\tgoogleHostedDomain: String!\n\tid: ID!\n\"\"\"An extra message that can be added the Authorization screen of an SSO Provider\"\"\"\n\tnote: String\n\torganization: Organization\n\"\"\"Defaults to false. If true, users are required to re-authenticate when their IP address changes.\"\"\"\n\tpinSessionToIpAddress: Boolean\n\"\"\"How long a session should last before requiring re-authorization. A `null` value indicates an infinite session.\"\"\"\n\tsessionDurationInHours: Int\n\"\"\"The current state of the SSO Provider\"\"\"\n\tstate: SSOProviderStates!\n\"\"\"Whether the SSO Provider requires a test authorization. If true, the provider can not yet be activated.\"\"\"\n\ttestAuthorizationRequired: Boolean\n\"\"\"The type of SSO Provider\"\"\"\n\ttype: SSOProviderTypes!\n\"\"\"The authorization URL for this SSO Provider\"\"\"\n\turl: String!\n\"\"\"The UUID for this SSO Provider\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"Single sign-on provided via SAML\"\"\"\ntype SSOProviderSAML implements Node & SSOProvider{\n\"\"\"The time when this SSO Provider was created\"\"\"\n\tcreatedAt: DateTime!\n\"\"\"The user that created this SSO Provider\"\"\"\n\tcreatedBy: User!\n\"\"\"The algorithm used to calculate the digest value during a SAML exchange\"\"\"\n\tdigestMethod: SSOProviderSAMLXMLSecurity!\n\"\"\"The time when this SSO Provider was disabled\"\"\"\n\tdisabledAt: DateTime\n\"\"\"The user that disabled this SSO Provider\"\"\"\n\tdisabledBy: User\n\"\"\"The reason this SSO Provider was disabled\"\"\"\n\tdisabledReason: String\n\"\"\"An email domain whose addresses should be offered this SSO Provider during login.\"\"\"\n\temailDomain: String\n\temailDomainVerificationAddress: String\n\temailDomainVerifiedAt: DateTime\n\"\"\"The time when this SSO Provider was enabled\"\"\"\n\tenabledAt: DateTime\n\"\"\"The user that enabled this SSO Provider\"\"\"\n\tenabledBy: User\n\tid: ID!\n\"\"\"Information about the IdP\"\"\"\n\tidentityProvider: SSOProviderSAMLIdPType\n\"\"\"An extra message that can be added the Authorization screen of an SSO Provider\"\"\"\n\tnote: String\n\torganization: Organization\n\"\"\"Defaults to false. If true, users are required to re-authenticate when their IP address changes.\"\"\"\n\tpinSessionToIpAddress: Boolean\n\tserviceProvider: SSOProviderSAMLSPType!\n\"\"\"How long a session should last before requiring re-authorization. A `null` value indicates an infinite session.\"\"\"\n\tsessionDurationInHours: Int\n\"\"\"The algorithm used to calculate the signature value during a SAML exchange\"\"\"\n\tsignatureMethod: SSOProviderSAMLRSAXMLSecurity!\n\"\"\"The current state of the SSO Provider\"\"\"\n\tstate: SSOProviderStates!\n\"\"\"Whether the SSO Provider requires a test authorization. If true, the provider can not yet be activated.\"\"\"\n\ttestAuthorizationRequired: Boolean\n\"\"\"The type of SSO Provider\"\"\"\n\ttype: SSOProviderTypes!\n\"\"\"The authorization URL for this SSO Provider\"\"\"\n\turl: String!\n\"\"\"The UUID for this SSO Provider\"\"\"\n\tuuid: ID!\n}\n\ninput SSOProviderSAMLIdP {\n\tissuer: String\n\tssoURL: String\n\tcertificate: String\n\tmetadata: SSOProviderSAMLIdPMetadata\n}\n\ninput SSOProviderSAMLIdPMetadata {\n\txml: XML\n\turl: String\n}\n\n\"\"\"Information about the IdP for a SAML SSO Provider\"\"\"\ntype SSOProviderSAMLIdPType {\n\"\"\"The certificated provided by the IdP\"\"\"\n\tcertificate: String\n\"\"\"The IdP Issuer value for this SSO Provider\"\"\"\n\tissuer: String\n\"\"\"The metadata used to configure this SSO provider if it was provided\"\"\"\n\tmetadata: SSOProviderSAMLMetadataType\n\"\"\"The name of the IdP Service. Returns nil if no name can be guessed from the SSO URL\"\"\"\n\tname: String\n\"\"\"The IdP SSO URL for this SSO Provider\"\"\"\n\tssoURL: String\n}\n\n\"\"\"SAML metadata used for configuration\"\"\"\ntype SSOProviderSAMLMetadataType {\n\"\"\"The URL that this metadata can be publicly accessed at\"\"\"\n\turl: String\n\"\"\"The XML for this metadata\"\"\"\n\txml: XML\n}\n\n\"\"\"XML RSA security algorithms used in the SAML exchange\"\"\"\nenum SSOProviderSAMLRSAXMLSecurity {\n\"\"\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"\"\"\n\tRSA_SHA1\n\"\"\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\"\"\"\n\tRSA_SHA256\n\"\"\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384\"\"\"\n\tRSA_SHA384\n\"\"\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512\"\"\"\n\tRSA_SHA512\n}\n\n\"\"\"Information about Buildkite as a SAML Service Provider\"\"\"\ntype SSOProviderSAMLSPType {\n\"\"\"The IdP Issuer value for this SSO Provider\"\"\"\n\tissuer: String\n\"\"\"The metadata used to configure this SSO provider if it was provided\"\"\"\n\tmetadata: SSOProviderSAMLMetadataType\n\"\"\"The IdP SSO URL for this SSO Provider\"\"\"\n\tssoURL: String\n}\n\n\"\"\"XML security algorithms used in the SAML exchange\"\"\"\nenum SSOProviderSAMLXMLSecurity {\n\"\"\"http://www.w3.org/2000/09/xmldsig#sha1\"\"\"\n\tSHA1\n\"\"\"http://www.w3.org/2001/04/xmlenc#sha256\"\"\"\n\tSHA256\n\"\"\"http://www.w3.org/2001/04/xmldsig-more#sha384\"\"\"\n\tSHA384\n\"\"\"http://www.w3.org/2001/04/xmlenc#sha512\"\"\"\n\tSHA512\n}\n\n\"\"\"All the possible states an SSO Provider can be in\"\"\"\nenum SSOProviderStates {\n\"\"\"The SSO Provider has been created, but has not been enabled for use yet\"\"\"\n\tCREATED\n\"\"\"The SSO Provider has been setup correctly and can be used by users\"\"\"\n\tENABLED\n\"\"\"The SSO Provider has been disabled and can't be used directly\"\"\"\n\tDISABLED\n}\n\n\"\"\"All the possible SSO Provider types\"\"\"\nenum SSOProviderTypes {\n\"\"\"An SSO Provider configured to use SAML\"\"\"\n\tSAML\n\"\"\"A SSO Provider configured to use Google G Suite for authorization\"\"\"\n\tGOOGLE_GSUITE\n\"\"\"A SSO Provider configured to use a GitHub App for authorization\"\"\"\n\tGITHUB_APP\n}\n\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\ninput SSOProviderUpdateInput {\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\tnote: String\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\tsessionDurationInHours: Int\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\tpinSessionToIpAddress: Boolean\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\temailDomain: String\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\temailDomainVerificationAddress: String\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\tidentityProvider: SSOProviderSAMLIdP\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\tdigestMethod: SSOProviderSAMLXMLSecurity\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\tsignatureMethod: SSOProviderSAMLRSAXMLSecurity\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\tgithubOrganizationName: String\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\tgoogleHostedDomain: String\n\"\"\"Autogenerated input type of SSOProviderUpdate\"\"\"\n\tdiscloseGoogleHostedDomain: Boolean\n}\n\n\"\"\"Autogenerated return type of SSOProviderUpdate.\"\"\"\ntype SSOProviderUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tssoProvider: SSOProvider!\n}\n\n\"\"\"A secret hosted by Buildkite. This does not contain the secret value or encrypted material.\"\"\"\ntype Secret implements Node{\n\"\"\"The cluster that the secret belongs to\"\"\"\n\tcluster: Cluster\n\"\"\"The time this secret was created\"\"\"\n\tcreatedAt: DateTime\n\"\"\"A description about what this secret is used for\"\"\"\n\tdescription: String\n\"\"\"The time this secret was destroyed\"\"\"\n\tdestroyedAt: DateTime\n\tid: ID!\n\"\"\"The key value used to name the secret\"\"\"\n\tkey: String!\n\"\"\"The organization that the secret belongs to\"\"\"\n\torganization: Organization!\n\"\"\"The time this secret was updated\"\"\"\n\tupdatedAt: DateTime\n\"\"\"The public UUID for the secret\"\"\"\n\tuuid: ID!\n}\n\ninterface Step {\n\tconditional: String\n\tdependencies: DependencyConnection\n\tkey: String\n\tuuid: String!\n}\n\n\"\"\"A step in a build that runs a command on an agent\"\"\"\ntype StepCommand implements Step{\n\"\"\"The conditional evaluated for this step\"\"\"\n\tconditional: String\n\"\"\"Dependencies of this job\"\"\"\n\tdependencies(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t): DependencyConnection\n\"\"\"The user-defined key for this step\"\"\"\n\tkey: String\n\"\"\"The UUID for this step\"\"\"\n\tuuid: String!\n}\n\n\"\"\"An input step collects information from a user\"\"\"\ntype StepInput implements Step{\n\"\"\"The conditional evaluated for this step\"\"\"\n\tconditional: String\n\"\"\"Dependencies of this job\"\"\"\n\tdependencies(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t): DependencyConnection\n\"\"\"The user-defined key for this step\"\"\"\n\tkey: String\n\"\"\"The UUID for this step\"\"\"\n\tuuid: String!\n}\n\n\"\"\"A trigger step creates a build on another pipeline\"\"\"\ntype StepTrigger implements Step{\n\"\"\"The conditional evaluated for this step\"\"\"\n\tconditional: String\n\"\"\"Dependencies of this job\"\"\"\n\tdependencies(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t): DependencyConnection\n\"\"\"The user-defined key for this step\"\"\"\n\tkey: String\n\"\"\"The UUID for this step\"\"\"\n\tuuid: String!\n}\n\n\"\"\"A wait step waits for all previous steps to have successfully completed before allowing following jobs to continue\"\"\"\ntype StepWait implements Step{\n\"\"\"The conditional evaluated for this step\"\"\"\n\tconditional: String\n\"\"\"Dependencies of this job\"\"\"\n\tdependencies(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t): DependencyConnection\n\"\"\"The user-defined key for this step\"\"\"\n\tkey: String\n\"\"\"The UUID for this step\"\"\"\n\tuuid: String!\n}\n\n\"\"\"Represents textual data as UTF-8 character sequences. This type is most often used by GraphQL to represent free-form human-readable text.\"\"\"\nscalar String\n\ntype Subscription {\n\tid: ID!\n}\n\n\"\"\"A suite\"\"\"\ntype Suite implements Node{\n\"\"\"The application name for the suite\"\"\"\n\tapplicationName: String\n\"\"\"The hex code for the suite navatar background color in the Test Suites page\"\"\"\n\tcolor: String\n\"\"\"The time when the suite was created\"\"\"\n\tcreatedAt: DateTime\n\"\"\"The default branch for this suite\"\"\"\n\tdefaultBranch: String\n\"\"\"The emoji that will display as a suite navatar in the Test Suites page\"\"\"\n\temoji: String\n\tid: ID!\n\"\"\"The name of the suite\"\"\"\n\tname: String!\n\torganization: Organization!\n\"\"\"The slug of the suite\"\"\"\n\tslug: String!\n\"\"\"Teams associated with this suite\"\"\"\n\tteams(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\"\"\"Search for teams associated that this suite is assigned to\"\"\"\n\t\tsearch: String\n\"\"\"Order the suites returned\"\"\"\n\t\torder: TeamSuiteOrder\n\t): TeamSuiteConnection\n\"\"\"The URL for the suite\"\"\"\n\turl: String!\n\tuuid: String!\n}\n\n\"\"\"The access levels that can be assigned to a suite\"\"\"\nenum SuiteAccessLevels {\n\"\"\"Allows edits and reads\"\"\"\n\tMANAGE_AND_READ\n\"\"\"Read only\"\"\"\n\tREAD_ONLY\n}\n\ntype SuiteConnection implements Connection{\n\tcount: Int!\n\tedges: [SuiteEdge]\n\tpageInfo: PageInfo\n}\n\ntype SuiteEdge {\n\tcursor: String!\n\tnode: Suite\n}\n\n\"\"\"The different orders you can sort suites by\"\"\"\nenum SuiteOrders {\n\"\"\"Order by name alphabetically\"\"\"\n\tNAME\n\"\"\"Order by the most recently created suites first\"\"\"\n\tRECENTLY_CREATED\n\"\"\"Order by relevance when searching for suites\"\"\"\n\tRELEVANCE\n}\n\n\"\"\"A TOTP configuration\"\"\"\ntype TOTP {\n\tid: ID!\n\"\"\"The recovery code batch associated with this TOTP configuration\"\"\"\n\trecoveryCodes: RecoveryCodeBatch!\n\"\"\"Whether the TOTP configuration has been verified yet\"\"\"\n\tverified: Boolean!\n}\n\n\"\"\"Autogenerated input type of TOTPActivate\"\"\"\ninput TOTPActivateInput {\n\"\"\"Autogenerated input type of TOTPActivate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TOTPActivate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of TOTPActivate\"\"\"\n\ttoken: String!\n}\n\n\"\"\"Autogenerated return type of TOTPActivate.\"\"\"\ntype TOTPActivatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\ttotp: TOTP!\n\tviewer: Viewer!\n}\n\n\"\"\"Autogenerated input type of TOTPCreate\"\"\"\ninput TOTPCreateInput {\n\"\"\"Autogenerated input type of TOTPCreate\"\"\"\n\tclientMutationId: String\n}\n\n\"\"\"Autogenerated return type of TOTPCreate.\"\"\"\ntype TOTPCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\"\"\"The URI to enter into your one-time password generator. Usually presented to the user as a QR Code\"\"\"\n\tprovisioningUri: String!\n\ttotp: TOTP!\n}\n\n\"\"\"Autogenerated input type of TOTPDelete\"\"\"\ninput TOTPDeleteInput {\n\"\"\"Autogenerated input type of TOTPDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TOTPDelete\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of TOTPDelete.\"\"\"\ntype TOTPDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tviewer: Viewer!\n}\n\n\"\"\"Autogenerated input type of TOTPRecoveryCodesRegenerate\"\"\"\ninput TOTPRecoveryCodesRegenerateInput {\n\"\"\"Autogenerated input type of TOTPRecoveryCodesRegenerate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TOTPRecoveryCodesRegenerate\"\"\"\n\ttotpId: ID!\n}\n\n\"\"\"Autogenerated return type of TOTPRecoveryCodesRegenerate.\"\"\"\ntype TOTPRecoveryCodesRegeneratePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\trecoveryCodes: RecoveryCodeBatch!\n\ttotp: TOTP!\n}\n\n\"\"\"An organization team\"\"\"\ntype Team implements Node{\n\"\"\"The time when this team was created\"\"\"\n\tcreatedAt: DateTime!\n\"\"\"The user that created this team\"\"\"\n\tcreatedBy: User\n\"\"\"New organization members will be granted this role on this team\"\"\"\n\tdefaultMemberRole: TeamMemberRole!\n\"\"\"A description of the team\"\"\"\n\tdescription: String\n\tid: ID!\n\"\"\"Add new organization members to this team by default\"\"\"\n\tisDefaultTeam: Boolean!\n\"\"\"Users that are part of this team\"\"\"\n\tmembers(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\"\"\"Search team members named like the given query case insensitively\"\"\"\n\t\tsearch: String\n\"\"\"Search team members by their role\"\"\"\n\t\trole: [TeamMemberRole!]\n\"\"\"Order the members returned\"\"\"\n\t\torder: TeamMemberOrder\n\t): TeamMemberConnection\n\"\"\"Whether or not team members can create new pipelines in this team\"\"\"\n\tmembersCanCreatePipelines: Boolean!\n\"\"\"Whether or not team members can delete pipelines in this team\"\"\"\n\tmembersCanDeletePipelines: Boolean!\n\"\"\"The name of the team\"\"\"\n\tname: String!\n\"\"\"The organization that this team is a part of\"\"\"\n\torganization: Organization\n\tpermissions: TeamPermissions!\n\"\"\"Pipelines associated with this team\"\"\"\n\tpipelines(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\"\"\"Search pipelines named like the given query case insensitively\"\"\"\n\t\tsearch: String\n\"\"\"Order the pipelines returned\"\"\"\n\t\torder: TeamPipelineOrder\n\t): TeamPipelineConnection\n\"\"\"The privacy setting for this team\"\"\"\n\tprivacy: TeamPrivacy!\n\"\"\"The slug of the team\"\"\"\n\tslug: String!\n\"\"\"Suites associated with this team\"\"\"\n\tsuites(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\"\"\"Order the suites returned\"\"\"\n\t\torder: TeamSuiteOrder\n\t): TeamSuiteConnection\n\"\"\"The public UUID for this team\"\"\"\n\tuuid: ID!\n}\n\ntype TeamConnection implements Connection{\n\tcount: Int!\n\tedges: [TeamEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of TeamCreate\"\"\"\ninput TeamCreateInput {\n\"\"\"Autogenerated input type of TeamCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamCreate\"\"\"\n\torganizationID: ID!\n\"\"\"Autogenerated input type of TeamCreate\"\"\"\n\tname: String!\n\"\"\"Autogenerated input type of TeamCreate\"\"\"\n\tdescription: String\n\"\"\"Autogenerated input type of TeamCreate\"\"\"\n\tprivacy: TeamPrivacy!\n\"\"\"Autogenerated input type of TeamCreate\"\"\"\n\tisDefaultTeam: Boolean!\n\"\"\"Autogenerated input type of TeamCreate\"\"\"\n\tdefaultMemberRole: TeamMemberRole!\n\"\"\"Autogenerated input type of TeamCreate\"\"\"\n\tmembersCanCreatePipelines: Boolean\n\"\"\"Autogenerated input type of TeamCreate\"\"\"\n\tmembersCanDeletePipelines: Boolean\n}\n\n\"\"\"Autogenerated return type of TeamCreate.\"\"\"\ntype TeamCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\torganization: Organization!\n\tteamEdge: TeamEdge!\n}\n\n\"\"\"Autogenerated input type of TeamDelete\"\"\"\ninput TeamDeleteInput {\n\"\"\"Autogenerated input type of TeamDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamDelete\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of TeamDelete.\"\"\"\ntype TeamDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedTeamID: ID!\n\torganization: Organization!\n}\n\ntype TeamEdge {\n\tcursor: String!\n\tnode: Team\n}\n\n\"\"\"An member of a team\"\"\"\ntype TeamMember implements Node{\n\"\"\"The time when the team member was added\"\"\"\n\tcreatedAt: DateTime!\n\"\"\"The user that added this team member\"\"\"\n\tcreatedBy: User\n\tid: ID!\n\"\"\"The organization member associated with this team member\"\"\"\n\torganizationMember: OrganizationMember\n\tpermissions: TeamMemberPermissions!\n\"\"\"The users role within the team\"\"\"\n\trole: TeamMemberRole!\n\"\"\"The team associated with this team member\"\"\"\n\tteam: Team\n\"\"\"The user associated with this team member\"\"\"\n\tuser: User\n\"\"\"The public UUID for this team member\"\"\"\n\tuuid: ID!\n}\n\ntype TeamMemberConnection implements Connection{\n\tcount: Int!\n\tedges: [TeamMemberEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of TeamMemberCreate\"\"\"\ninput TeamMemberCreateInput {\n\"\"\"Autogenerated input type of TeamMemberCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamMemberCreate\"\"\"\n\tteamID: ID!\n\"\"\"Autogenerated input type of TeamMemberCreate\"\"\"\n\tuserID: ID!\n\"\"\"Autogenerated input type of TeamMemberCreate\"\"\"\n\trole: TeamMemberRole\n}\n\n\"\"\"Autogenerated return type of TeamMemberCreate.\"\"\"\ntype TeamMemberCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tteam: Team\n\tteamMemberEdge: TeamMemberEdge\n}\n\n\"\"\"Autogenerated input type of TeamMemberDelete\"\"\"\ninput TeamMemberDeleteInput {\n\"\"\"Autogenerated input type of TeamMemberDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamMemberDelete\"\"\"\n\tid: ID!\n}\n\n\"\"\"Autogenerated return type of TeamMemberDelete.\"\"\"\ntype TeamMemberDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedTeamMemberID: ID!\n\tteam: Team\n}\n\ntype TeamMemberEdge {\n\tcursor: String!\n\tnode: TeamMember\n}\n\n\"\"\"The different orders you can sort team members by\"\"\"\nenum TeamMemberOrder {\n\"\"\"Order by name alphabetically\"\"\"\n\tNAME\n\"\"\"Order by most relevant results when doing a search\"\"\"\n\tRELEVANCE\n\"\"\"Order by the most recently added members first\"\"\"\n\tRECENTLY_CREATED\n}\n\n\"\"\"Permissions information about what actions the current user can do against the team membership record\"\"\"\ntype TeamMemberPermissions {\n\"\"\"Whether the user can delete the user from the team\"\"\"\n\tteamMemberDelete: Permission\n\"\"\"Whether the user can update the team's members admin status\"\"\"\n\tteamMemberUpdate: Permission\n}\n\n\"\"\"The roles a user can be within a team\"\"\"\nenum TeamMemberRole {\n\"\"\"The user is a regular member of the team\"\"\"\n\tMEMBER\n\"\"\"The user can manage pipelines and users within the team\"\"\"\n\tMAINTAINER\n}\n\n\"\"\"Autogenerated input type of TeamMemberUpdate\"\"\"\ninput TeamMemberUpdateInput {\n\"\"\"Autogenerated input type of TeamMemberUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamMemberUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of TeamMemberUpdate\"\"\"\n\trole: TeamMemberRole!\n}\n\n\"\"\"Autogenerated return type of TeamMemberUpdate.\"\"\"\ntype TeamMemberUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tteamMember: TeamMember!\n}\n\n\"\"\"The different orders you can sort teams by\"\"\"\nenum TeamOrder {\n\"\"\"Order by name alphabetically\"\"\"\n\tNAME\n\"\"\"Order by the most recently created teams first\"\"\"\n\tRECENTLY_CREATED\n\"\"\"Order by relevance when searching for teams\"\"\"\n\tRELEVANCE\n}\n\n\"\"\"Permissions information about what actions the current user can do against the team\"\"\"\ntype TeamPermissions {\n\"\"\"Whether the user can see the pipelines within the team\"\"\"\n\tpipelineView: Permission\n\"\"\"Whether the user can delete the team\"\"\"\n\tteamDelete: Permission\n\"\"\"Whether the user can administer add members from the organization to this team\"\"\"\n\tteamMemberCreate: Permission\n\"\"\"Whether the user can add pipelines from other teams to this one\"\"\"\n\tteamPipelineCreate: Permission\n\"\"\"Whether the user can add suites from other teams to this one\"\"\"\n\tteamSuiteCreate: Permission\n\"\"\"Whether the user can update the team's name and description\"\"\"\n\tteamUpdate: Permission\n}\n\n\"\"\"An pipeline that's been assigned to a team\"\"\"\ntype TeamPipeline implements Node{\n\"\"\"The access level users have to this pipeline\"\"\"\n\taccessLevel: PipelineAccessLevels!\n\"\"\"The time when the pipeline was added\"\"\"\n\tcreatedAt: DateTime!\n\"\"\"The user that added this pipeline to the team\"\"\"\n\tcreatedBy: User\n\tid: ID!\n\tpermissions: TeamPipelinePermissions!\n\"\"\"The pipeline associated with this team member\"\"\"\n\tpipeline: Pipeline\n\"\"\"The team associated with this team member\"\"\"\n\tteam: Team\n\"\"\"The public UUID for this team member\"\"\"\n\tuuid: ID!\n}\n\n\"\"\"A collection of TeamPipeline records\"\"\"\ntype TeamPipelineConnection implements Connection{\n\tcount: Int!\n\tedges: [TeamPipelineEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of TeamPipelineCreate\"\"\"\ninput TeamPipelineCreateInput {\n\"\"\"Autogenerated input type of TeamPipelineCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamPipelineCreate\"\"\"\n\tteamID: ID!\n\"\"\"Autogenerated input type of TeamPipelineCreate\"\"\"\n\tpipelineID: ID!\n\"\"\"Autogenerated input type of TeamPipelineCreate\"\"\"\n\taccessLevel: PipelineAccessLevels\n}\n\n\"\"\"Autogenerated return type of TeamPipelineCreate.\"\"\"\ntype TeamPipelineCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tpipeline: Pipeline\n\tteam: Team\n\tteamPipeline: TeamPipeline\n\tteamPipelineEdge: TeamPipelineEdge\n}\n\n\"\"\"Autogenerated input type of TeamPipelineDelete\"\"\"\ninput TeamPipelineDeleteInput {\n\"\"\"Autogenerated input type of TeamPipelineDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamPipelineDelete\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of TeamPipelineDelete\"\"\"\n\tforce: Boolean\n}\n\n\"\"\"Autogenerated return type of TeamPipelineDelete.\"\"\"\ntype TeamPipelineDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedTeamPipelineID: ID!\n\tteam: Team\n}\n\ntype TeamPipelineEdge {\n\tcursor: String!\n\tnode: TeamPipeline\n}\n\n\"\"\"The different orders you can sort pipelines by\"\"\"\nenum TeamPipelineOrder {\n\"\"\"Order by name alphabetically\"\"\"\n\tNAME\n\"\"\"Order by most relevant results when doing a search\"\"\"\n\tRELEVANCE\n\"\"\"Order by the most recently added pipelines first\"\"\"\n\tRECENTLY_CREATED\n}\n\n\"\"\"Permission information about what actions the current user can do against the team pipelines\"\"\"\ntype TeamPipelinePermissions {\n\"\"\"Whether the user can delete the pipeline from the team\"\"\"\n\tteamPipelineDelete: Permission\n\"\"\"Whether the user can update the pipeline connection to the team\"\"\"\n\tteamPipelineUpdate: Permission\n}\n\n\"\"\"Autogenerated input type of TeamPipelineUpdate\"\"\"\ninput TeamPipelineUpdateInput {\n\"\"\"Autogenerated input type of TeamPipelineUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamPipelineUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of TeamPipelineUpdate\"\"\"\n\taccessLevel: PipelineAccessLevels!\n}\n\n\"\"\"Autogenerated return type of TeamPipelineUpdate.\"\"\"\ntype TeamPipelineUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tteamPipeline: TeamPipeline!\n}\n\n\"\"\"Whether a team is visible or secret within an organization\"\"\"\nenum TeamPrivacy {\n\"\"\"Visible to all members of the organization\"\"\"\n\tVISIBLE\n\"\"\"Visible to organization administrators and members\"\"\"\n\tSECRET\n}\n\n\"\"\"A Team identifier using a slug, and optionally negated with a leading `!`\"\"\"\nscalar TeamSelector\n\n\"\"\"A suite that's been assigned to a team\"\"\"\ntype TeamSuite implements Node{\n\"\"\"The access level users have to this suite\"\"\"\n\taccessLevel: SuiteAccessLevels!\n\"\"\"The time when the suite was added\"\"\"\n\tcreatedAt: DateTime!\n\"\"\"The user that added this suite to the team\"\"\"\n\tcreatedBy: User\n\tid: ID!\n\tpermissions: TeamSuitePermissions!\n\"\"\"The suite associated with this team member\"\"\"\n\tsuite: Suite\n\"\"\"The team associated with this team member\"\"\"\n\tteam: Team\n\"\"\"The public UUID for this team suite\"\"\"\n\tuuid: String!\n}\n\n\"\"\"A collection of TeamSuite records\"\"\"\ntype TeamSuiteConnection implements Connection{\n\tcount: Int!\n\tedges: [TeamSuiteEdge]\n\tpageInfo: PageInfo\n}\n\n\"\"\"Autogenerated input type of TeamSuiteCreate\"\"\"\ninput TeamSuiteCreateInput {\n\"\"\"Autogenerated input type of TeamSuiteCreate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamSuiteCreate\"\"\"\n\tteamID: ID!\n\"\"\"Autogenerated input type of TeamSuiteCreate\"\"\"\n\tsuiteID: ID!\n\"\"\"Autogenerated input type of TeamSuiteCreate\"\"\"\n\taccessLevel: SuiteAccessLevels\n}\n\n\"\"\"Autogenerated return type of TeamSuiteCreate.\"\"\"\ntype TeamSuiteCreatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tsuite: Suite\n\tteam: Team\n\tteamSuite: TeamSuite\n\tteamSuiteEdge: TeamSuiteEdge\n}\n\n\"\"\"Autogenerated input type of TeamSuiteDelete\"\"\"\ninput TeamSuiteDeleteInput {\n\"\"\"Autogenerated input type of TeamSuiteDelete\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamSuiteDelete\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of TeamSuiteDelete\"\"\"\n\tforce: Boolean\n}\n\n\"\"\"Autogenerated return type of TeamSuiteDelete.\"\"\"\ntype TeamSuiteDeletePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tdeletedTeamSuiteID: ID!\n\tteam: Team\n}\n\ntype TeamSuiteEdge {\n\tcursor: String!\n\tnode: TeamSuite\n}\n\n\"\"\"The different orders you can sort suites by\"\"\"\nenum TeamSuiteOrder {\n\"\"\"Order by name alphabetically\"\"\"\n\tNAME\n\"\"\"Order by most relevant results when doing a search\"\"\"\n\tRELEVANCE\n\"\"\"Order by the most recently added suites first\"\"\"\n\tRECENTLY_CREATED\n}\n\n\"\"\"Permission information about what actions the current user can do against the team suites\"\"\"\ntype TeamSuitePermissions {\n\"\"\"Whether the user can delete the suite from the team\"\"\"\n\tteamSuiteDelete: Permission\n\"\"\"Whether the user can update the suite connection to the team\"\"\"\n\tteamSuiteUpdate: Permission\n}\n\n\"\"\"Autogenerated input type of TeamSuiteUpdate\"\"\"\ninput TeamSuiteUpdateInput {\n\"\"\"Autogenerated input type of TeamSuiteUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamSuiteUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of TeamSuiteUpdate\"\"\"\n\taccessLevel: SuiteAccessLevels!\n}\n\n\"\"\"Autogenerated return type of TeamSuiteUpdate.\"\"\"\ntype TeamSuiteUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tteamSuite: TeamSuite!\n}\n\n\"\"\"Autogenerated input type of TeamUpdate\"\"\"\ninput TeamUpdateInput {\n\"\"\"Autogenerated input type of TeamUpdate\"\"\"\n\tclientMutationId: String\n\"\"\"Autogenerated input type of TeamUpdate\"\"\"\n\tid: ID!\n\"\"\"Autogenerated input type of TeamUpdate\"\"\"\n\tname: String!\n\"\"\"Autogenerated input type of TeamUpdate\"\"\"\n\tdescription: String\n\"\"\"Autogenerated input type of TeamUpdate\"\"\"\n\tprivacy: TeamPrivacy\n\"\"\"Autogenerated input type of TeamUpdate\"\"\"\n\tisDefaultTeam: Boolean!\n\"\"\"Autogenerated input type of TeamUpdate\"\"\"\n\tdefaultMemberRole: TeamMemberRole!\n\"\"\"Autogenerated input type of TeamUpdate\"\"\"\n\tmembersCanCreatePipelines: Boolean\n\"\"\"Autogenerated input type of TeamUpdate\"\"\"\n\tmembersCanDeletePipelines: Boolean\n}\n\n\"\"\"Autogenerated return type of TeamUpdate.\"\"\"\ntype TeamUpdatePayload {\n\"\"\"A unique identifier for the client performing the mutation.\"\"\"\n\tclientMutationId: String\n\tteam: Team!\n}\n\n\"\"\"A record of test executions usage, aggregated by day and test suite.\"\"\"\ntype TestExecutionsUsage implements ResourceUsageInterface{\n\taggregatedOn: ISO8601Date!\n\"\"\"The recorded usage.\"\"\"\n\texecutions: Int!\n\tsuite: Suite\n\tsuiteId: ID!\n}\n\n\"\"\"A person who hasn’t signed up to Buildkite\"\"\"\ntype UnregisteredUser {\n\tavatar: Avatar!\n\"\"\"The email for the user\"\"\"\n\temail: String\n\"\"\"The name of the user\"\"\"\n\tname: String\n}\n\n\"\"\"The possible resource usage types\"\"\"\nunion UsageUnion =JobMinutesUsage | TestExecutionsUsage\n\n\"\"\"The connection type for UsageUnion.\"\"\"\ntype UsageUnionConnection {\n\"\"\"A list of edges.\"\"\"\n\tedges: [UsageUnionEdge]\n\"\"\"A list of nodes.\"\"\"\n\tnodes: [UsageUnion]\n\"\"\"Information to aid in pagination.\"\"\"\n\tpageInfo: PageInfo!\n}\n\n\"\"\"An edge in a connection.\"\"\"\ntype UsageUnionEdge {\n\"\"\"A cursor for use in pagination.\"\"\"\n\tcursor: String!\n\"\"\"The item at the end of the edge.\"\"\"\n\tnode: UsageUnion\n}\n\n\"\"\"A user\"\"\"\ntype User implements Node{\n\tavatar: Avatar!\n\"\"\"If this user account is an official bot managed by Buildkite\"\"\"\n\tbot: Boolean!\n\"\"\"Returns builds that this user has created.\"\"\"\n\tbuilds(\n\t\tfirst: Int\n\t\tlast: Int\n\t\tstate: [BuildStates!]\n\t\tbranch: [String!]\n\t\tmetaData: [String!]\n\t): BuildConnection\n\"\"\"The primary email for the user\"\"\"\n\temail: String!\n\"\"\"Does the user have a password set\"\"\"\n\thasPassword: Boolean!\n\tid: ID!\n\"\"\"The name of the user\"\"\"\n\tname: String!\n\"\"\"The public UUID of the user\"\"\"\n\tuuid: String!\n}\n\n\"\"\"A User identifier using a UUID, and optionally negated with a leading `!`\"\"\"\nscalar UserSelector\n\n\"\"\"Represents the current user session\"\"\"\ntype Viewer implements Node{\n\tauthorizations(\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\t\ttype: [AuthorizationType!]\n\t): AuthorizationConnection\n\tbuilds(\n\t\tfirst: Int\n\t\tlast: Int\n\t\tstate: [BuildStates!]\n\t\tbranch: String\n\t\tmetaData: [String!]\n\t): BuildConnection\n\tchangelogs(\n\t\tfirst: Int\n\t\tlast: Int\n\t\tread: Boolean\n\t): ChangelogConnection\n\"\"\"Emails associated with the current user\"\"\"\n\temails(\n\"\"\"Returns the elements in the list that come after the specified cursor.\"\"\"\n\t\tafter: String\n\"\"\"Returns the elements in the list that come before the specified cursor.\"\"\"\n\t\tbefore: String\n\"\"\"Returns the first _n_ elements from the list.\"\"\"\n\t\tfirst: Int\n\"\"\"Returns the last _n_ elements from the list.\"\"\"\n\t\tlast: Int\n\"\"\"Filter by whether the email is verified or not\"\"\"\n\t\tverified: Boolean\n\t): EmailConnection\n\"\"\"The ID of the current user\"\"\"\n\tid: ID!\n\tjobs(\n\t\tfirst: Int\n\t\tafter: String\n\t\tlast: Int\n\t\tbefore: String\n\t\ttype: [JobTypes!]\n\t\tstate: [JobStates!]\n\t\tpriority: JobPrioritySearch\n\t\tagentQueryRules: [String!]\n\"\"\"Order the jobs\"\"\"\n\t\torder: JobOrder\n\t): JobConnection\n\tnotice(\n\t\tnamespace: NoticeNamespaces!\n\t\tscope: String!\n\t): Notice\n\torganizations(\n\t\tfirst: Int\n\t\tlast: Int\n\t): OrganizationConnection\n\"\"\"The current user's permissions\"\"\"\n\tpermissions: ViewerPermissions!\n\"\"\"The user's active TOTP configuration, if any.\n\nThis field is private, requires an escalated session, and cannot be accessed via the public GraphQL API.\"\"\"\n\ttotp(\n\t\tid: ID\n\t): TOTP\n\"\"\"The current user\"\"\"\n\tuser: User\n}\n\n\"\"\"Permissions information about what actions the current user can do\"\"\"\ntype ViewerPermissions {\n\"\"\"Whether the viewer can configure two-factor authentication\"\"\"\n\ttotpConfigure: Permission!\n}\n\n\"\"\"A blob of XML represented as a pretty formatted string\"\"\"\nscalar XML\n\n\"\"\"A blob of YAML\"\"\"\nscalar YAML\n\n"
  }
]